--- Input for CODER at 20250925_075634 ---
================================ Human Message =================================

- META THOUGHTS: Analyzer confirms the root cause: foo_player selects the first playable action with no evaluation or planning. To improve performance we must implement a 1-ply lookahead that simulates each legal action on a copied game state and scores resulting states with adapters.make_value_fn if available, otherwise use a robust heuristic combining VPs, settlements, cities, resources, and production potential. Add logging, tie-breaking, and safe fallbacks if adapters functions differ or throw. If code raises import/syntax errors during the next run, call RESEARCHER to fix adapter references. 
- META GOAL: Replace the current action selection with a 1-ply simulation and state-evaluation pipeline so FooPlayer picks actions by estimated value rather than position in the action list, aiming to increase average VP and start winning some games.
- CHOSEN AGENT: CODER
- AGENT OBJECTIVE: Implement the following changes to foo_player.py:

1) Replace the naive "return playable_actions[0]" behavior with a 1-ply lookahead evaluator:
   - For each action in playable_actions:
     - Create a copy of the current game using adapters.copy_game (use try/except to handle missing API; if not available, attempt a shallow copy approach or skip simulation).
     - Apply the candidate action to the copied game state. Use the common adapters pattern (e.g., adapters.simulate_action or copying then calling game.apply_action). Because exact adapter function names may vary, implement a try/except sequence:
       a) Try: adapters.copy_game(game); then call copied_game.do_action(action) or copied_game.apply_action(action) inside try/except.
       b) If direct call fails, try adapters.apply_action(copy, action), adapters.play_action(copy, action), or simulate the action by setting up expected attributes. Catch exceptions and, on failure, skip simulation for that action (or assign a conservative low score).
   - Evaluate the resulting state:
     - Prefer using adapters.make_value_fn if available:
         try: vf = adapters.make_value_fn(game=current_copied_game, ...) or adapters.make_value_fn(); score = vf(copied_game)
         Fallback: compute heuristic_score(copied_game, player_index) using:
           * Weighted sum: VP * 100 + settlements*20 + cities*50 + roads*5 + army*10.
           * Add small resource potential term: sum(resource_counts) * 1.
           * Optionally include expected production: for each hex owned by player's settlements/cities, add weight proportional to hex probability (use hardcoded die probabilities for numbers 2-12).
   - Select the action with the highest score. Break ties randomly (use random.choice over tied best actions).

2) Implementation details and functions to add:
   - evaluate_action(game, action, player_index) -> float
       * Performs copy, apply, evaluate, and returns a numeric score. Catches exceptions and returns -inf or a very low number on failure to ensure it's not chosen.
   - heuristic_value(game, player_index) -> float
       * Extract player's VP, settlements, cities, roads, resources, and compute weighted sum.
   - robust_copy_and_apply(game, action) -> copied_game or None
       * Encapsulates trying different adapter/application methods with informative debug logging to help diagnose failures.
   - Use adapters.* calls within try/except; do not import catanatron internal modules directly.

3) Logging and debugging:
   - Add debug prints (or use existing logging in project) showing chosen action, scores of top candidate actions, and fallbacks used. Keep logs concise to avoid flooding.

4) Safety and fallbacks:
   - If no playable_actions, return None or the provided pass action.
   - If simulations fail for all actions, default to original behavior (first action) to avoid stalling.
   - Ensure code imports adapters at top and catches ImportError with a clear fallback.

5) Testing/Instrumenting:
   - Ensure code runs without syntax errors.
   - Keep code self-contained — do not call the game runner. The test harness will run the game.

6) Performance considerations:
   - Limit simulation overhead: if playable_actions is large, cap simulations to top N actions sampled randomly or heuristically (e.g., N=12).
   - Make evaluation fast: avoid deep copies beyond what's needed.

Please implement these modifications in foo_player.py. Provide clear comments for areas where adapter function names may need adjusting based on the environment. If the next run raises attribute or import errors regarding adapters.copy_game or action application, I will call RESEARCHER to get precise adapter usage.
================================== Ai Message ==================================

CODER

- Rewrote foo_player.py to implement a 1-ply lookahead using only the adapters API.
- Key changes made:
  - Added imports from .adapters (Game, Player, Color, execute_deterministic, execute_spectrum, base_fn).
  - Implemented FooPlayer that caches adapters.base_fn() if available; falls back to a heuristic evaluator.
  - Added helper functions:
    - _get_player_color: robust retrieval of player's color attribute.
    - _heuristic_value: fallback heuristic scoring based on VP, settlements, cities, roads, resources.
    - _evaluate_game_state: uses cached value function or heuristic to score a Game for a Color.
    - _evaluate_action_expectation: expands an action using adapters.execute_deterministic or execute_spectrum and computes expected score.
  - Updated decide to:
    - Convert playable_actions to a list, handle empty lists.
    - Sample actions if the action space is large (cap 16).
    - Evaluate each candidate action's expected value and choose the best (break ties randomly).
    - Provide debug print statements for tracing decisions and fallback usage.
- Debugging / logging:
  - Multiple print statements added to aid diagnosis (value_fn availability, execution fallbacks, per-action scores, chosen action).
- Performance safeguards:
  - Limits simulations to MAX_SIMULATIONS=16 when the candidate list is large.
  - Returns first playable action if all evaluations fail.

Notes / Questions for next iteration:
- I used adapters.execute_deterministic and adapters.execute_spectrum as provided. If the environment uses different adapter names for applying/validating actions, provide the exact adapter signatures so I can refine error handling.
- The heuristic inspects game.state.players or game.players and common attribute names; if the real game model uses different attribute names for player stats, please provide them so the heuristic can be more precise.
- If you want less verbose logging, I can reduce print statements or add a debug flag.

END CODER
================================ Human Message =================================

- META THOUGHTS: Analyzer shows real improvement (8 wins) but still losing on average. Root causes: 1-ply is too myopic for long-term constructs (city upgrades, road networks, robber impact) and current sampling can miss critical actions. We need targeted changes: smarter action sampling that guarantees inclusion of strategically important actions, a light multi-step lookahead (selective 2-ply or short greedy rollout) to estimate downstream value, and a richer heuristic/value function that accounts for production potential, city upgrade distance, and disruptive moves (robber/knight/dev cards). Keep fallbacks robust and keep runtime bounded.
- META GOAL: Improve foo_player.py so it reliably picks high long-term-value moves by (a) always evaluating all high-impact actions, (b) using a short greedy rollout from simulated child states (depth 2-3) to estimate downstream effects, and (c) enhancing the heuristic/value function to include production potential, city upgrade progress, and robber/knight value. Keep simulations bounded (adaptive sampling + must-include list) and reduce noisy logging.

- CHOSEN AGENT: CODER

- AGENT OBJECTIVE:
Implement the following concrete, backwards-compatible improvements in foo_player.py. Preserve the adapters-based approach and all previous fallbacks; add targeted enhancements and keep runtime safeguards.

1) New high-level approach
   - Keep the existing 1-ply simulation path as baseline but augment it with:
     a) Prefilter/prioritization to always include high-impact actions in the simulation set.
     b) For each simulated child state, run a short greedy rollout (depth ROLLOUT_DEPTH, default 2 or 3) using a fast heuristic policy to approximate downstream value (select best heuristic action for the acting player at each rollout step).
     c) Use the expected value across chance branches (execute_spectrum or execute_deterministic) for the first move and then add the rollout-estimated value to get action score.

2) Action prefiltering & adaptive sampling
   - Introduce constants:
       MAX_SIMULATIONS = 24 (cap)
       MUST_INCLUDE_TYPES = {'BUILD_CITY','BUILD_SETTLEMENT','BUILD_ROAD','BUY_DEV_CARD','PLAY_KNIGHT','MOVE_ROBBER','TRADE'} (handle string variants)
       PREFILTER_TOP_K = 8 (after must-includes, pick top K by cheap pre-score)
       ROLLOUT_DEPTH = 2 (default; allow tuning)
   - Implement prefilter_actions(playable_actions, game, player_index):
       a) Compute a cheap pre-score for every action without copying (cheap_pre_score):
           - Score +100 for any action that directly increases VP (BUILD_CITY, BUILD_SETTLEMENT).
           - Score +60 for BUY_DEV_CARD.
           - Score +40 for BUILD_ROAD if it extends existing roads or connects to potential settlement sites (best effort: check if action string contains an edge index adjacent to player's settlements).
           - Score +50 for MOVE_ROBBER or PLAY_KNIGHT.
           - Score adjustment for trades based on resource imbalance (e.g., if lacks key city resources).
       b) Collect must_include actions by matching action.type, action.name, or substrings of str(action) against MUST_INCLUDE_TYPES; ensure robust matching (lowercase).
       c) Sort remaining actions by cheap_pre_score, pick top PREFILTER_TOP_K.
       d) Return final candidate_actions list = unique(must_includes + top_prefiltered), then if len < MAX_SIMULATIONS, append random samples from remaining actions to reach min(len(all), MAX_SIMULATIONS).

   - Make matching resilient: check hasattr(action,'type') and hasattr(action,'name'), else fallback to str(action).lower() contains token.

3) Rollout-based downstream estimation
   - Implement rollout_value(copied_game, player_color, depth):
       a) For depth = 0 return evaluate_game_state(copied_game, player_color) using cached adapters.base_fn() if available else heuristic.
       b) Otherwise, for the current game state determine playable actions for the active player (use adapters.execute_deterministic with empty action? If you already have a method to get playable_actions from the provided game object, use that; else use game.get_playable_actions or adapters.* — robust try/except).
       c) If playable_actions empty: return evaluate_game_state.
       d) Choose the best action according to the cheap pre-score (no copying) for that player, apply it deterministically on a shallow copy (or use execute_deterministic to resolve chance when required), then recursively call rollout_value on the new state with depth-1.
       e) If cannot simulate an action, skip it and try the next best; if none simulate, return evaluate_game_state.
       f) Return the evaluation value from the leaf.

   - Note: We only need a quick approximate rollout — keep copies/shallow simulations fast and avoid branching across many chance nodes during rollout (use deterministic simulation selected by adapters.execute_deterministic or one representative branch from execute_spectrum).

4) Enhanced heuristic / value function
   - Update heuristic_value(game, player_color) to include:
       a) Base terms: Victory points * 100, settlements * 25, cities * 60, roads * 6, army size * 15, dev_vp * 50.
       b) Production potential: For each settlement/city of player, add weight proportional to hex probability:
           - Use die_probabilities dict: {2:1/36,3:2/36,4:3/36,5:4/36,6:5/36,8:5/36,9:4/36,10:3/36,11:2/36,12:1/36}. (Ignore 7.)
           - City adds double production weight of a settlement.
       c) City upgrade progress: estimate resources towards next city (e.g., if player has cities < #settlements, compute required wheat+ore shortfall and subtract from score proportionally).
       d) Resource diversity & monopoly: reward unique resource types held (diversity_count * 2) and reward higher count for scarce city-building resources (ore, wheat).
       e) Robber impact: penalize if a player's best-producing hex is blocked by robber (detect occupant if possible).
   - Keep using adapters.make_value_fn() if available — prefer it. If using both, combine by weighted average: 0.8*value_fn + 0.2*heuristic for stability.

5) Robber/knight specific evaluation
   - When prefilter identifies a MOVE_ROBBER or PLAY_KNIGHT action, expand expected value taking into account:
       a) Which opponent hex is targeted — prefer hexes that reduce opponent production score most (compute opponent production loss using die_prob).
       b) If steal is possible, add estimated expected stolen resource value (map resources to build-weights).
   - Ensure robber moves are always included in candidate_actions (must_include).

6) Improve evaluate_action_expectation
   - For each candidate action:
       a) Use adapters.execute_spectrum(action, game) if it exists to get (prob, resulting_state) branches; else use adapters.execute_deterministic or try to copy/apply.
       b) For each branch, compute branch_value = evaluate_game_state(branch_state, player_color) for immediate scoring, plus rollout_value(branch_state, player_color, ROLLOUT_DEPTH-1) if doing rollouts — sum or average appropriately.
       c) Expected_value = sum(prob * branch_value).
   - If execute_spectrum unavailable, fallback to deterministic path and an approximate exploration of chance: e.g., run single deterministic simulation then adjust with small variance term.

7) Sampling & performance safeguards
   - Limit total simulated branches across actions to a budget: SIMULATION_BUDGET = 60 (approx). If candidate action count * avg_branches exceeds budget, reduce ROLLOUT_DEPTH dynamically or cap per-action branches to budget / num_actions.
   - Avoid deep copying the whole game more than necessary; reuse copy methods provided by adapters.

8) Robustness & adapter compatibility
   - Keep the existing try/except cascade for calling adapters.copy_game, game.apply_action / do_action, adapters.execute_deterministic, adapters.execute_spectrum, adapters.make_value_fn.
   - When checking action types, handle multiple naming conventions gracefully.
   - Add a single global DEBUG flag to control printing; default to False to reduce log noise.

9) Logging & instrumentation
   - Replace many prints with debug_print(...) that checks DEBUG flag.
   - Log: chosen action, top 3 candidate actions with their expected scores, any fallback used (e.g., no execute_spectrum), and if sampling was used. Keep logs concise.

10) Fallbacks and safe returns
   - If all evaluations fail or candidate list empty, return the original behavior (first playable_action) to avoid stalling.
   - Ensure none of the new helpers raise uncaught exceptions — wrap top-level decide() body in try/except and fallback gracefully.

11) Tests / debug hooks (for future RESEARCHER use)
   - Add small helper to emit a summary of why an action was included (must_include reason or pre-score) when DEBUG True.
   - If you encounter adapter attribute errors in the next run, include the exception trace in logs and we will call RESEARCHER to map exact adapter function names.

Deliverable specifics:
   - Modify foo_player.py to add these functions: prefilter_actions, cheap_pre_score, rollout_value, evaluate_action_expectation (enhanced), enhanced heuristic_value, debug_print, and constants at top.
   - Keep existing code structure but replace the sampling + evaluation block with the new pipeline.
   - Maintain use of adapters and never import catanatron internals directly.

If you implement this, run the next experiment. If runtime errors about adapter names or member access occur, stop and report the exact AttributeError/Traceback so I can call RESEARCHER to get precise adapter signatures.
================================== Ai Message ==================================

CODER

- What I changed (high level)
  - Replaced the original simple first-action behavior with an enhanced decision pipeline that:
    - Prefilters the playable actions to ensure high-impact actions are always considered.
    - Uses adapters.execute_spectrum / adapters.execute_deterministic to expand chance outcomes for each candidate action.
    - Evaluates each outcome using adapters.base_fn() when available, otherwise a richer fallback heuristic.
    - Runs a short greedy rollout (depth-limited) from each outcome to estimate downstream value.
    - Selects the action with highest expected score and breaks ties randomly.
  - All interactions with the game use functions exported from .adapters (per requirement).

- Files changed
  - Overwrote foo_player.py with the new implementation.

- New functions and helpers added to foo_player.py
  - debug_print(*args)
    - Single place to gate debug prints via DEBUG flag (default False).
  - _safe_action_name(action)
    - Robust conversion of an action to a lowercase string for token matching.
  - _heuristic_value(game, color)
    - Replaced / enhanced heuristic that now includes:
      - VP, settlements, cities, roads, dev VP, resources and resource diversity.
      - Production potential estimated from adjacent hex roll numbers (best-effort, uses die probabilities).
      - City-upgrade progress proxy via wheat & ore counts.
      - Tuned weightings so the heuristic prefers production and VP.
  - _evaluate_game_state(game, color)
    - Uses adapters.base_fn() when available and blends it with the heuristic (0.85*value_fn + 0.15*heuristic).
    - Falls back to heuristic if base_fn raises.
  - cheap_pre_score(action, game, color)
    - Fast, cheap scoring used to prioritize which actions should be simulated.
  - prefilter_actions(actions, game, color)
    - Builds bounded candidate list to simulate:
      - Always includes must-include tokens (e.g., build_city, move_robber).
      - Picks top PREFILTER_TOP_K by cheap_pre_score.
      - Fills up to MAX_SIMULATIONS with random samples to preserve diversity.
  - get_playable_actions_from_game(game)
    - Best-effort extraction of playable actions via adapters.list_prunned_actions or common game fields.
  - rollout_value(game, color, depth)
    - Short greedy rollout (selects the best cheap_pre_score action at each step) to estimate downstream value.
    - Uses deterministic branch (or most probable branch from spectrum) and recurses depth-1.
  - _evaluate_action_expectation(game, action, per_action_branch_limit=8)
    - Enhanced expectation calculator:
      - Tries execute_spectrum first, falls back to execute_deterministic.
      - Limits per-action branch expansion to keep runtime bounded.
      - For each branch: compute blended branch value = 0.6*immediate_eval + 0.4*rollout_estimate.
      - Returns normalized expected value.
  - decide(game, playable_actions)
    - Orchestrates prefiltering, budget allocation (SIMULATION_BUDGET), per-action evaluation, and selection with tie-breaking.

- Constants and tunables added
  - MAX_SIMULATIONS = 24
  - PREFILTER_TOP_K = 8
  - ROLLOUT_DEPTH = 2
  - SIMULATION_BUDGET = 60
  - MUST_INCLUDE_TOKENS set to prioritize high-impact action types
  - DEBUG flag (default False) to control logging verbosity

- Logging / prints added
  - Mainly guarded by debug_print (uses DEBUG flag). Default logging remains quiet.
  - One protected fallback print remains in the top-level exception in decide() to ensure errors are visible (this will not flood when DEBUG is False).
  - When DEBUG=True, the player prints:
    - Whether adapters.base_fn() was successfully used.
    - Prefilter selection summary.
    - Evaluation results per action and top candidates.
    - Any fallbacks used (execute_spectrum->execute_deterministic, value_fn failures, etc).

- Safety / fallbacks implemented
  - Robust try/except around adapters calls:
    - execute_spectrum and execute_deterministic both tried and failure returns -inf for that action (so it is unlikely to be chosen).
  - If no playable_actions: return None.
  - If all simulations/evaluations fail, fallback to the first playable action to avoid stalling.
  - Decision pipeline wrapped in try/except to ensure a sensible fallback if unexpected exceptions occur.

- Performance considerations
  - Candidate actions limited via prefilter to MAX_SIMULATIONS.
  - Branch expansion per action limited by per_action_branch_limit computed from SIMULATION_BUDGET.
  - Rollout depth set small (default 2) for speed; rollout picks only up to 4 top actions at each step to avoid branching blowup.
  - cheap_pre_score and prefilter are deliberately lightweight (no copying).

- Debugging aids / notes for the next iteration
  - The production estimation and location lookup are best-effort guesses: code inspects game.state.board.hexes and player settlement/city lists (settle_locations, city_locations). If your real game model uses different field names or structures for board/hexes/vertex adjacency, the production term may be inaccurate.
  - The MUST_INCLUDE_TOKENS matching is string-based and intended to be tolerant. If the game's Action objects include ActionType enums (e.g., ActionType.BUILD_CITY), the _safe_action_name should capture .name but confirm the exact naming used by the adapters Action type in your environment.
  - The blend weights between immediate evaluation and rollout (0.6/0.4) and between base_fn/heuristic (0.85/0.15) are heuristic and can be tuned.
  - The DEBUG flag is off by default; set FooPlayer.DEBUG = True within tests if you want verbose logs to diagnose decisions.

- Things I am unsure about / that may need RESEARCHER help next run
  - Exact structure of player_state and board/hex adjacency in the concrete Game implementation:
    - The code attempts to find settlement/city location lists under many common names (settlements, settle_locations, city_locations). If those attributes are named differently, production and upgrade-progression heuristics will be weakened.
  - Action naming conventions:
    - If the Action object exposes action_type via a different attribute name or the string repr differs significantly, must-include matching may miss some actions. If you get AttributeError or strange behavior referencing action.action_type, provide the exact Action class fields.
  - If adapters exposes additional or different helper functions for enumerating playable actions (e.g., another exported list function), let me know and I will prefer it.

- Suggested next objectives (for future iterations)
  - Tune heuristic weights and rollout blending using small hyperparameter search (e.g., adjust production weight & city rewards).
  - Implement smarter robber evaluation: evaluate target hex impact on opponent production and expected stolen resources to score robber moves more accurately.
  - Add a selective 2-ply search for build_city/build_settlement actions where the second ply considers a short opponent response or a self-progression move.
  - Add specialized logic for trading and dev-card purchases (when model shows underinvestment in dev cards).
  - If desired, expose DEBUG via environment variable or player constructor argument.

- Changes summary (concise bullets)
  - Created new helper functions: debug_print, _safe_action_name, cheap_pre_score, prefilter_actions, get_playable_actions_from_game, rollout_value, and enhanced _evaluate_action_expectation.
  - Replaced naive decide logic with prefilter -> evaluate (spectrum/deterministic) -> rollout -> choose best.
  - Added improved heuristic and blending with adapters.base_fn() when present.
  - Added guarded debug logging; default is quiet.

If you run the next experiment and see AttributeError or missing adapter names (for example, execute_spectrum, list_prunned_actions, base_fn, etc.), please paste the exact traceback and I will request RESEARCHER to map the exact adapter function names or update the code to match the environment.

END CODER
================================ Human Message =================================

- META THOUGHTS: Analyzer shows we reached a strong improvement (17/30 wins) but still tilt toward city-heavy play that neglects early expansion (settlements/roads), robber/knight disruption, and dev-card investment. To push win rate higher we should add game-phase-aware decisioning: favor settlements/roads early, cities mid/ to late-game, and explicitly evaluate robber/knight moves and buy-dev-card actions. Keep the existing safe adapters-based simulation and rollout pipeline, but change cheap_pre_score and heuristic blending to be dynamic by phase, add targeted evaluators for robber/knight and settlement potential, and tune sampling to always include these action types. Keep runtime safeguards and DEBUG gating.

- META GOAL: Update foo_player.py so FooPlayer:
  1) prefers settlements and roads in the early game (so it doesn't starve resource diversity),
  2) evaluates robber/knight actions for disruption and potential steals,
  3) considers buying development cards when appropriate,
  4) preserves the successful rollout + spectrum evaluation pipeline and budget limits.

- CHOSEN AGENT: CODER

- AGENT OBJECTIVE:
Implement the following concrete changes in foo_player.py. Keep all adapter calls in try/except fallbacks as before. Preserve DEBUG flag (default False) and simulation-budget protections.

1) Game-phase detection
   - Add helper is_early_game(game, color_or_index):
     - Determine the current overall progression using robust checks (try in order):
         a) game.turn or game.tick or game.state.turn_count or game.state.tick_count
         b) If none available, use average victory points across players: if max VP < 5 treat as early.
     - Return True for early when turn_count < EARLY_TURN_THRESHOLD (default 25) OR when max VP < 5.
   - Add constants:
       EARLY_TURN_THRESHOLD = 25
       EARLY_VP_THRESHOLD = 5

2) Dynamic scoring multipliers
   - Add dynamic multipliers applied in cheap_pre_score and _heuristic_value:
       - If is_early_game -> settlement_multiplier = 1.6, road_multiplier = 1.4, city_multiplier = 0.9, devcard_multiplier = 1.1
       - Else (mid/late) -> settlement_multiplier = 0.9, road_multiplier = 0.9, city_multiplier = 1.4, devcard_multiplier = 1.0
   - Use these multipliers when scoring build actions: multiply base score for BUILD_SETTLEMENT, BUILD_ROAD, BUILD_CITY, BUY_DEV_CARD.

3) Settlement & road potential evaluation
   - Implement settlement_potential(action, game, color) returning a small float bonus estimating:
       - Resource diversity gain: how many new resource types would the settlement add (best-effort by checking adjacent hex resource types).
       - Production potential: sum die probability weights for adjacent hex numbers; treat city adjacency as double.
       - Distance to existing player's network: reward connecting distant nodes less than connecting existing network more? (Simpler: slightly prefer settlement actions that increase unique resources).
   - Implement road_connection_potential(action, game, color) returning float:
       - Reward if road action appears to extend toward an open settlement spot (best-effort: check edges adjacent to known free vertices). If exact board API missing, reward road actions if their string contains indices near player's settlement locations.

4) Robber & Knight evaluation
   - Add evaluate_robber_action(action, game, color):
       - When action moves robber, attempt to detect target hex id from action and estimate production loss to the most affected opponent:
           * For each opponent, compute their production score (sum of probabilities of their settlements/cities); estimate reduction if that hex is blocked (subtract die_prob for that hex times sum of settlement/city weights).
           * Add steal-value: if action results in a steal (detect from action or branch result), add estimated expected resource value (map resource -> build-weight; e.g., ore/wheat=3, lumber/brick/sheep=2).
       - Return robbery_score large enough to make robbery moves be included (e.g., +40 base scaled by estimated impact).
   - Add evaluate_play_knight(action, game, color):
       - Similar to robber evaluation but also account for increasing largest army or potential VP from army. Include small bonus if player is close to largest army threshold.

5) Development card buys
   - In cheap_pre_score, boost BUY_DEV_CARD by devcard_multiplier and add heuristic to buy when:
       - Player has moderate resources (e.g., enough ore + wheat and no immediate city needed) OR when early_game and strategy prefers hidden advantage.
       - If player already has many cities, slightly deprioritize devcard buys in favor of settlement opportunities as appropriate.

6) Modify prefilter_actions to guarantee inclusion
   - Expand MUST_INCLUDE_TOKENS to ensure:
       - build_settlement and build_road actions are always included if early_game is True (use is_early_game to decide).
       - move_robber and play_knight included when present.
       - buy_dev_card included when resources sufficient (check action costs if present in action object or check player resource counts).
   - Keep existing PREFILTER_TOP_K and MAX_SIMULATIONS but ensure must-includes are always present even if they exceed PREFILTER_TOP_K (still respect MAX_SIMULATIONS cap by replacing low-scored sampled actions).

7) Rollout policy tweaks
   - In rollout_value:
       - When early_game True and rollout depth > 0, bias greedy rollout selection toward settlement/road/build actions using cheap_pre_score with phase multipliers (so rollout represents early expansion).
       - When mid/late-game, bias toward city upgrades and dev-card plays.
   - Keep rollout depth default 2 but allow ROLLOUT_DEPTH to be passed from constants.

8) Heuristic tuning
   - Update _heuristic_value to incorporate dynamic multipliers and stronger production-term weighting for early game:
       - Increase production-term weight in early game to favor settlements that increase resource flow.
       - Slightly reduce immediate city reward during early game to avoid over-prioritizing upgrades that hamper expansion.
   - Combine adapters.base_fn when present as before but ensure dynamic multipliers still influence final score: final_score = 0.85*value_fn + 0.15*heuristic * phase_multiplier_sum or apply phase multipliers inside heuristic then combine.

9) Logging and DEBUG
   - Keep DEBUG default False. When DEBUG True, print concise lines:
       - "PHASE: EARLY/MID/LATE, chosen multipliers: S/R/C"
       - Top 3 candidate actions and their expected scores
       - If a robber action is chosen, print target hex and estimated impact.
   - Avoid noisy per-action prints when DEBUG False.

10) Testing & fallbacks
   - Ensure all new helper functions are robust to missing attributes: check multiple attribute names for resources, victory points, settlements/cities lists (player.vp, player.victory_points, player.victory_point).
   - If determining adjacent hexes or edge/vertex indices fails, fallback to string-based heuristics but do not crash.
   - If evaluate_robber_action cannot find a target hex, it should return a small neutral score (not negative).
   - Maintain final fallback behavior: if all action evaluations fail, return playable_actions[0].

11) Minor tunables to expose as constants for future tuning:
   - EARLY_TURN_THRESHOLD (default 25)
   - EARLY_VP_THRESHOLD (default 5)
   - settlement_multiplier_early, road_multiplier_early, etc.
   - ROBBER_BASE_SCORE (default 40)
   - DEV_CARD_MULTIPLIER_EARLY (default 1.1)

Implementation notes for CODER:
- Only modify foo_player.py. Do not import internal catanatron modules.
- Use existing try/except patterns for adapters.execute_spectrum, execute_deterministic, adapters.base_fn, adapters.copy_game.
- Keep simulation budget and per-action branch limits as before, adjusting if necessary to accommodate the extra computation but aim to keep runtime similar.
- Add unit-level comments where you detect uncertainty about attribute names so RESEARCHER can later refine them if adapter attribute mismatches occur.
- After implementing, leave DEBUG=False by default.

If you implement these changes and the next run shows attribute/adapter errors (AttributeError, etc.), capture the traceback exactly and return it so we can call RESEARCHER to map the correct adapter names or attribute paths.
================================== Ai Message ==================================

CODER

I updated foo_player.py with the requested phase-aware decisioning, enhanced heuristic, targeted action sampling, robber/knight evaluation, and rollout biases. Key points:

- Implemented is_early_game to detect early/mid phases.
- Adjusted cheap_pre_score and _heuristic_value to use phase multipliers favoring settlements/roads early and cities later.
- Added settlement_potential and road_connection_potential to encourage expansion.
- Added evaluate_robber_action and evaluate_play_knight to boost disruptive moves.
- Prefilter ensures must-include actions and early-game settlement/road inclusion.
- Rollout policy biases towards expansion in early game.
- All adapters calls use the provided adapters surface and are wrapped in try/except fallbacks.
- DEBUG flag is available (default False) to enable concise diagnostic prints.

Next steps:
- Run the experiments. If you see AttributeError traces or adapter mismatches (e.g., missing functions or different attribute names on game/player/hex objects), paste the exact traceback and I will work with RESEARCHER to adapt the code.

END CODER
================================ Human Message =================================

- META THOUGHTS: think_tool confirms progress and identifies remaining weaknesses: insufficient early expansion (settlements/roads), underinvestment in development cards, and weak robber/knight exploitation. We should make targeted, low-risk changes: stronger early-game multipliers favoring settlement/road, explicit dev-card buy logic when appropriate, more accurate robber/knight valuation (production loss + expected steal + army progress), and make rollout explicitly prefer expansion in the first rollout step. Keep simulation budget and adapter-fallbacks intact.

- META GOAL: Modify foo_player.py to (1) more strongly prefer settlements and roads in early game, (2) add dev-card buy prioritization when beneficial, (3) improve robber/knight evaluation to capture disruption and steals, and (4) bias rollout to expansion early — while preserving performance safeguards and adapter fallback logic.

- CHOSEN AGENT: CODER

- AGENT OBJECTIVE:
Implement the following precise changes inside foo_player.py. Keep all adapter calls wrapped in try/except and keep DEBUG default False.

1) Stronger early-game multipliers & forced inclusion
   - Change constants:
       EARLY_SETTLEMENT_MULT = 2.0 (was 1.6)
       EARLY_ROAD_MULT = 1.8 (was 1.4)
       EARLY_CITY_MULT = 0.8
       EARLY_DEV_MULT = 1.2
   - In prefilter_actions, add logic:
       - If is_early_game(game, player_color) is True:
           * Ensure at least one BUILD_SETTLEMENT and one BUILD_ROAD action (if present in playable_actions) are included among candidates before sampling — i.e., scan playable_actions and, if found, add them to must_include set even if their cheap_pre_score is low.
           * If no settlement or road action present, no-op (do not add missing actions).

2) Settlement potential reward for new resource types
   - In settlement_potential(action, game, color), compute:
       - adjacent_resources = set(types of resources on hexes adjacent to the settlement vertex proposed by action)
       - player_resources_owned_types = set(resource types adjacent to the player's existing settlements/cities)
       - new_types = adjacent_resources - player_resources_owned_types
       - reward = len(new_types) * 12.0 + sum(die_probabilities[number] for adjacent numbers) * 8.0
   - Add this settlement_potential to cheap_pre_score for BUILD_SETTLEMENT actions, multiplied by EARLY_SETTLEMENT_MULT when early.

3) Road connection increase for expansion
   - road_connection_potential(action, game, color) should:
       - Reward if the road connects from an existing player's node toward an open vertex (best-effort adjacency check).
       - If exact adjacency unknown, use heuristic: if road action's string/index references an edge adjacent to any player settlement vertex, reward +6; if it references an edge that is not adjacent to any player structure, reward +3.
   - Multiply by EARLY_ROAD_MULT when early.

4) Development card prioritization
   - Add function should_buy_dev_card(action, game, color):
       - If action indicates BUY_DEV_CARD or similar:
           * If player has at least (ORE>=1 and WHEAT>=1 and another resource) or if not enough immediate high-value build options exist, return True.
           * If early_game: give a modest bonus (early devs can be good) but only when not blocking settlement creation.
       - cheap_pre_score should add base +25 to BUY_DEV_CARD when should_buy_dev_card returns True; multiply by EARLY_DEV_MULT if early.

5) Enhanced robber/knight evaluation
   - evaluate_robber_action(action, game, color):
       - Try to identify target_hex_id from action (safe parsing).
       - For each opponent:
           * Compute opponent_production = sum(probabilities of hex numbers adjacent to their settlements/cities, weighting cities double).
           * If target_hex is adjacent to opponent's structures, compute production_loss = die_prob[target_hex_number] * (1 for settlement / 2 for city) summed across opponent structures affected.
       - steal_expected_value: if steal likely (detectable in action/branch), estimate expected resource value = sum(resource_value_map[r] * probability_of_steal_resource_of_type_r).
           * Use resource_value_map: {ore:3, wheat:3, brick:2, lumber:2, sheep:2}
       - robber_score = ROBBER_BASE_SCORE + sum(production_loss across opponents)*30 + steal_expected_value*10
       - Return robber_score (scale as needed).
   - evaluate_play_knight(action, game, color):
       - Similar to robber but include army_progress: if player.army_count exists, compute if this play will move toward largest army threshold (e.g., award +20 if it creates or increases chance).
       - Combine with production loss and steal estimates.

6) Rollout bias adjustment
   - In rollout_value, at the first rollout step (depth == initial_rollout_depth), if is_early_game True:
       - When selecting best action in greedy rollout, apply additional weight: settlement_bonus_for_rollout = 1.5, road_bonus_for_rollout = 1.3 to cheap_pre_score when ranking actions to choose for the rollout.
   - For deeper steps, use normal cheap_pre_score.

7) Tuning & tie-breaking
   - When selecting the final action, prefer actions that improve resource diversity and production in event of score ties (use settlement_potential and production terms). If still tied, random.choice.

8) Safety & performance
   - Keep MAX_SIMULATIONS = 24 and SIMULATION_BUDGET = 60. If adding the new computations increases CPU, reduce per-action branch limit appropriately.
   - Ensure any failure in parsing action details (target hex, adjacent vertices) returns neutral small score rather than exception.

9) Logging
   - When DEBUG True, print:
       - Early-game forced includes found: yes/no, list them.
       - For chosen action if it's BUY_DEV_CARD, MOVE_ROBBER, or PLAY_KNIGHT, print reason: e.g., "Chosen ROBBER -> expected opponent production loss X, steal_ev Y".
       - Top 3 candidates with expected values.

10) Minimal code changes pointers for CODER
   - Update constants at top.
   - Modify prefilter_actions to add force include logic when early_game.
   - Update cheap_pre_score to call settlement_potential and road_connection_potential and to use new multipliers.
   - Add should_buy_dev_card and incorporate into cheap_pre_score.
   - Implement evaluate_robber_action and evaluate_play_knight and ensure they're invoked in cheap_pre_score or during evaluate_action_expectation to boost candidate scores for robber/knight moves.
   - Modify rollout_value to bias first step in early_game.
   - Ensure all functions robustly handle missing attributes and use adapters to read game/player state where possible.

If you implement these changes, run the next experiment. If there are attribute errors or adapter mismatches, capture the full traceback and we'll call RESEARCHER for exact adapter signatures or attribute paths.
================================== Ai Message ==================================

CODER

Changes made to foo_player.py (summary)
- Implemented the full set of phase-aware, rollout-augmented decisioning changes requested by Analyzer/META.
- Preserved strict use of the adapters surface: all interactions with the game use functions/objects imported from .adapters and are surrounded by try/except fallbacks.
- Kept DEFAULT DEBUG = False to avoid noisy logs; added guarded debug_print helper.

New/modified constants and tuning
- MAX_SIMULATIONS = 24
- PREFILTER_TOP_K = 8
- ROLLOUT_DEPTH = 2
- SIMULATION_BUDGET = 60
- EARLY_TURN_THRESHOLD = 25
- EARLY_VP_THRESHOLD = 5
- EARLY_SETTLEMENT_MULT = 2.0
- EARLY_ROAD_MULT = 1.8
- EARLY_CITY_MULT = 0.8
- EARLY_DEV_MULT = 1.2
- LATE_* multipliers (settlement/road/city/dev) preserved as before
- ROBBER_BASE_SCORE = 40.0

New / changed functions (high level)
- debug_print(*args)
  - Gate for logging controlled by FooPlayer.DEBUG (default False). Used everywhere for concise conditional logging.

- _get_player_color()
  - Robust retrieval of this player's Color, used by evaluation and decision code.

- _safe_action_name(action)
  - Returns a robust lowercased string for an Action object by trying common attributes (.action_type/.type/.name) and fallback to str(action). Used for token matching.

- is_early_game(game)
  - Heuristic to detect early game using turn/tick or fallback to max VP among players.

- _heuristic_value(game, color)
  - Phase-aware heuristic combining:
    - VP, settlements, cities, roads, dev VP
    - Resource totals & diversity
    - Production potential estimated from adjacent hex numbers (die probabilities)
    - City upgrade progress (wheat/ore shortfall)
    - Phase multipliers that bias settlement/road vs city/dev production
  - Phase multipliers updated to stronger early bias (increased production weight in early game).

- _evaluate_game_state(game, color)
  - Prefer adapters.base_fn() when available (cached in constructor) and blend with heuristic: 0.85 * value_fn + 0.15 * heuristic. Falls back to heuristic on errors.

- _get_player_state(game, color)
  - Best-effort extraction of the player's state object from the game.

- settlement_potential(action, game, color)
  - Best-effort evaluation of a settlement action:
    - Parses a vertex index (if present in action string),
    - Identifies adjacent hex resources and roll numbers,
    - Computes new resource types gained compared to existing player resource types,
    - Returns float bonus = new_types * 12 + prod_sum * 8 (+ small base).

- road_connection_potential(action, game, color)
  - Best-effort evaluation of road action connectivity:
    - Extracts digits from action string and checks if they match player nodes; rewards connecting to own nodes (+6) else smaller reward (+3).
    - Small fallback for string-based detection of road actions.

- should_buy_dev_card(action, game, color)
  - Best-effort rule to determine whether BUY_DEV_CARD is sensible based on player's current resource counts (ore + wheat + another resource). Used in cheap_pre_score.

- cheap_pre_score(action, game, color)
  - Phase-aware fast scoring used to prefilter and rank actions.
  - Now includes:
    - Stronger early multipliers (EARLY_*),
    - Calls to settlement_potential and road_connection_potential,
    - Dev-card preference (should_buy_dev_card): high score if buying makes sense,
    - Robber/knight quick boosts via evaluate_robber_action / evaluate_play_knight.

- prefilter_actions(actions, game, color)
  - Ensures must-include tokens are present.
  - If early game: force inclusion of at least one settlement and one road action (if present in playable_actions).
  - Picks top PREFILTER_TOP_K by cheap_pre_score and fills up to MAX_SIMULATIONS with random samples.
  - Emits concise debug lines when DEBUG True.

- get_playable_actions_from_game(game)
  - Tries list_prunned_actions() first, then common game attributes (.get_playable_actions, .playable_actions, state.playable_actions).

- evaluate_robber_action(action, game, color)
  - Best-effort robber impact estimator:
    - Parses target hex id from action string if present,
    - Builds a hex_map from board hexes (index/id fallback),
    - For target hex: computes total production loss across opponents (based on adjacent settlements/cities) and estimates steal expected value,
    - Score = ROBBER_BASE_SCORE + 30 * total_prod_loss + 10 * steal_expected (float).

- evaluate_play_knight(action, game, color)
  - Best-effort knight estimator:
    - Adds small base,
    - Detects steal/rob keywords,
    - Awards extra if player army count suggests approaching largest army (bonus if army >= 2).

- rollout_value(game, color, depth, initial=True)
  - Short greedy rollout (depth-limited). If initial==True and early game, biases the first rollout step toward settlement and road actions (1.5x/1.3x multipliers applied when ranking actions).
  - Uses execute_deterministic() first and falls back to execute_spectrum() for simulating chosen rollout action.

- _evaluate_action_expectation(game, action, per_action_branch_limit=8)
  - Uses execute_spectrum() when available; falls back to execute_deterministic().
  - Limits branches to per_action_branch_limit, computes per-branch blended value: 0.6 * immediate_eval + 0.4 * rollout_est (rollout depth = ROLLOUT_DEPTH - 1), averages across branches, then adds a preboost for robber/knight quick estimate.
  - Returns -inf if both execution methods fail.

- decide(game, playable_actions)
  - Orchestrates the pipeline:
    - Prefilter candidates (phase-aware must-includes),
    - Cap candidates to MAX_SIMULATIONS,
    - Distribute SIMULATION_BUDGET across candidates (per_action_budget),
    - Evaluate each candidate via _evaluate_action_expectation,
    - Tie-breaking: if multiple best actions, prefer actions with higher settlement_potential/road_connection_potential and production proxy; finally random.choice among equals.
    - Debug logging is concise and controlled by DEBUG.

Safety, fallbacks, and robustness
- All adapters interactions (execute_spectrum, execute_deterministic, list_prunned_actions, base_fn) are wrapped in try/except and handled gracefully.
- Attribute access throughout is defensive: multiple attribute names attempted (players, state, board, hexes, resource names, roll/number/value). Where parsing fails, best-effort string heuristics are used instead of raising.
- When parsing numeric indices from action strings, code catches parsing errors and falls back to safe default behavior.
- If all evaluations fail, decide() falls back to returning the first playable action (so the agent never stalls).

Logging changes
- Introduced debug_print() used in many places. DEBUG default is False.
- When DEBUG True, prints:
  - Whether adapters.base_fn() was found,
  - Prefilter selection counts and early forced-includes,
  - Top candidates and phase info in decide(),
  - Robber/knight-specific messages in evaluate_robber_action/evaluate_play_knight where helpful.
- The top-level exception handler in decide() still prints errors unconditionally (ensures visibility if something critical goes wrong).

Performance considerations
- MAX_SIMULATIONS, PREFILTER_TOP_K, ROLLOUT_DEPTH, and SIMULATION_BUDGET govern runtime. Branch limits are applied per action to keep time bounded.
- cheap_pre_score and potentials are designed to be lightweight but informative.
- Rollouts use only deterministic or the most probable branch per branch list to avoid explosion in branching during rollouts.

What I created/added (explicit list)
- New functions:
  - debug_print
  - _get_player_color
  - _safe_action_name
  - is_early_game
  - _heuristic_value (enhanced weighting & phase multipliers)
  - _evaluate_game_state
  - _get_player_state
  - settlement_potential
  - road_connection_potential
  - should_buy_dev_card
  - cheap_pre_score (updated)
  - prefilter_actions (updated force-inclusion behavior)
  - get_playable_actions_from_game
  - evaluate_robber_action (new detailed estimator)
  - evaluate_play_knight (new estimator)
  - rollout_value (updated: initial bias param)
  - _evaluate_action_expectation (updated blending and preboost)
  - decide (orchestrator updated for tie-breaking and forced includes)

Debug/print statements added
- Small guarded debug_print messages scattered across the file; DEBUG default False.
- One unconditional print remains in the decide() exception fallback to ensure errors are visible.

Areas of uncertainty / where RESEARCHER help may be needed
- Exact Action object structure and naming:
  - The code uses string parsing of action objects (action_type, type, name, or str(action)). If the actual Action objects expose differently-named attributes or a richer structure (e.g., Action.action_type is an enum with .name), this code attempts to detect them but may miss structured data. If AttributeError arises pointing to a precise action attribute, provide the stack trace and I will update parsing to use that attribute.
- Player state structure and board representation:
  - The code uses many common attribute names (players, state.players, player.resources, player.settlements, board.hexes, h.vertices/h.adjacent_vertices, etc.). If your concrete Game model uses different names, the production and settlement/road potential estimators will underperform. Provide the actual attribute names/structures if you see misses.
- Robber/hex id mapping:
  - The logic tries to map hex indices or ids found in action string to hex objects. If the Action encodes hex ids differently (e.g., via a nested payload), we should adapt to parse that payload directly. Provide example action repr or traceback if issues occur.
- compute of steal_expected in evaluate_robber_action:
  - It's a best-effort estimate using opponent resource dicts. If opponent resource dict keys differ, or if stealing behavior is encoded differently, the estimate may be noisy.

Suggested next tuning steps (if you want further improvement)
- Tune EARLY_* multipliers, prod_weight in _heuristic_value, and blending weights between value_fn and heuristic (0.85/0.15).
- If dev-card buys remain underused, increase buy pref scores slightly or lower city multiplier early.
- If robber/knight moves appear underutilized, further increase ROBBER_BASE_SCORE or scaling factors in evaluate_robber_action.
- If action parsing shows consistent structure, replace string-based parsing with direct attribute accesses to improve accuracy.

Report of direct code edits
- Rewrote much of decide prefilter / scoring / rollout functions to implement:
  - Forced inclusion of settlement/road during early game
  - Settlement potential and road connection heuristics
  - Dev-card buy heuristics
  - Robber/knight evaluators
  - Rollout initial bias toward expansion in early game

If next run raises AttributeError or adapter mismatches
- Please paste the full traceback (stack trace). I will call RESEARCHER to obtain exact adapter function names or object attribute layouts and then update the parsing/access accordingly. The code is defensive, but exact matches to the game's action and state structure will improve performance.

END CODER
================================ Human Message =================================

ANALYZER
---
After Running The New foo_player.py Player, Here is my analysis and findings:

---
### PERFORMANCE SUMMARY:
- **Outcome**: FooPlayer won **20 out of 30 games** (66.67% win rate).
- **VP**: FooPlayer:BLUE averaged **8.63 VP** vs. AlphaBetaPlayer:RED's **6.87 VP** (VP diff: **+1.76**).
- **Key Counts**:
  - Settlements: 2.43 (FooPlayer) vs. 2.53 (Opponent).
  - Cities: 2.53 (FooPlayer) vs. 1.73 (Opponent).
  - Roads: 0.57 (FooPlayer) vs. 0.43 (Opponent).
  - Development Cards: 0.0 (FooPlayer) vs. 0.0 (Opponent).
  - Total Turns: 70.87.

---
### VERDICT:
- **Good**: FooPlayer achieved a **positive VP difference of +1.76** and won the majority of games.

---
### STRENGTHS:
1. **City Dominance**: FooPlayer excels at building cities (2.53 vs. 1.73), directly contributing to higher VP.
2. **Phase-Aware Strategy**: The heuristic and rollout logic adapt to early/mid/late game phases, prioritizing cities and settlements appropriately.
3. **Robust Action Sampling**: The prefiltering logic ensures high-impact actions (e.g., `build_city`, `build_settlement`) are prioritized.
4. **Rollout Estimation**: The 2-ply rollout provides a short-term strategic advantage, as seen in the VP lead.

---
### LIKELY REASONS FOR SUBOPTIMAL PERFORMANCE:
1. **Settlement Deficit**: FooPlayer builds slightly fewer settlements (2.43 vs. 2.53), which may limit early-game expansion and resource diversity.
   - **Code Citation**: The heuristic prioritizes cities over settlements in mid/late game (Lines 200–210, `foo_player.py`).
   - **Log Corroboration**: No explicit log lines indicate aggressive settlement placement in early game.

2. **Road Neglect**: Roads are built at a similar rate to the opponent (0.57 vs. 0.43), which may limit long-term expansion and connectivity.
   - **Code Citation**: Road-building actions are scored lower in `cheap_pre_score` (Line 120, `foo_player.py`).

3. **Development Card Underutilization**: Despite a slight edge in development cards (0.0 vs. 0.0), FooPlayer does not actively prioritize or leverage them.
   - **Code Citation**: No specialized logic for development card actions in `cheap_pre_score` or rollout.

4. **Robber/Knight Actions**: No evidence of strategic robber placement or knight usage, which could disrupt the opponent's resource flow.
   - **Log Corroboration**: No log lines indicate robber/knight actions.

---
### STRATEGIC FLAW:
FooPlayer's heuristic and rollout logic prioritize cities and immediate VP gains but neglect early-game expansion (settlements/roads) and disruptive actions (robber/knights), limiting long-term dominance.

---
### NEXT STEP:
Send to Coder to **enhance the heuristic to prioritize settlements and roads early-game** and **add logic for strategic robber/knight usage**.

---
Let me know if you need anything else.
END ANALYZER
================================== Ai Message ==================================

- META THOUGHTS: think_tool confirms progress and identifies remaining weaknesses: insufficient early expansion (settlements/roads), underinvestment in development cards, and weak robber/knight exploitation. We should make targeted, low-risk changes: stronger early-game multipliers favoring settlement/road, explicit dev-card buy logic when appropriate, more accurate robber/knight valuation (production loss + expected steal + army progress), and make rollout explicitly prefer expansion in the first rollout step. Keep simulation budget and adapter-fallbacks intact.

- META GOAL: Modify foo_player.py to (1) more strongly prefer settlements and roads in early game, (2) add dev-card buy prioritization when beneficial, (3) improve robber/knight evaluation to capture disruption and steals, and (4) bias rollout to expansion early — while preserving performance safeguards and adapter fallback logic.

- CHOSEN AGENT: CODER

- AGENT OBJECTIVE:
Implement the following precise changes inside foo_player.py. Keep all adapter calls wrapped in try/except and keep DEBUG default False.

1) Stronger early-game multipliers & forced inclusion
   - Change constants:
       EARLY_SETTLEMENT_MULT = 2.0 (was 1.6)
       EARLY_ROAD_MULT = 1.8 (was 1.4)
       EARLY_CITY_MULT = 0.8
       EARLY_DEV_MULT = 1.2
   - In prefilter_actions, add logic:
       - If is_early_game(game, player_color) is True:
           * Ensure at least one BUILD_SETTLEMENT and one BUILD_ROAD action (if present in playable_actions) are included among candidates before sampling — i.e., scan playable_actions and, if found, add them to must_include set even if their cheap_pre_score is low.
           * If no settlement or road action present, no-op (do not add missing actions).

2) Settlement potential reward for new resource types
   - In settlement_potential(action, game, color), compute:
       - adjacent_resources = set(types of resources on hexes adjacent to the settlement vertex proposed by action)
       - player_resources_owned_types = set(resource types adjacent to the player's existing settlements/cities)
       - new_types = adjacent_resources - player_resources_owned_types
       - reward = len(new_types) * 12.0 + sum(die_probabilities[number] for adjacent numbers) * 8.0
   - Add this settlement_potential to cheap_pre_score for BUILD_SETTLEMENT actions, multiplied by EARLY_SETTLEMENT_MULT when early.

3) Road connection increase for expansion
   - road_connection_potential(action, game, color) should:
       - Reward if the road connects from an existing player's node toward an open vertex (best-effort adjacency check).
       - If exact adjacency unknown, use heuristic: if road action's string/index references an edge adjacent to any player settlement vertex, reward +6; if it references an edge that is not adjacent to any player structure, reward +3.
   - Multiply by EARLY_ROAD_MULT when early.

4) Development card prioritization
   - Add function should_buy_dev_card(action, game, color):
       - If action indicates BUY_DEV_CARD or similar:
           * If player has at least (ORE>=1 and WHEAT>=1 and another resource) or if not enough immediate high-value build options exist, return True.
           * If early_game: give a modest bonus (early devs can be good) but only when not blocking settlement creation.
       - cheap_pre_score should add base +25 to BUY_DEV_CARD when should_buy_dev_card returns True; multiply by EARLY_DEV_MULT if early.

5) Enhanced robber/knight evaluation
   - evaluate_robber_action(action, game, color):
       - Try to identify target_hex_id from action (safe parsing).
       - For each opponent:
           * Compute opponent_production = sum(probabilities of hex numbers adjacent to their settlements/cities, weighting cities double).
           * If target_hex is adjacent to opponent's structures, compute production_loss = die_prob[target_hex_number] * (1 for settlement / 2 for city) summed across opponent structures affected.
       - steal_expected_value: if steal likely (detectable in action/branch), estimate expected resource value = sum(resource_value_map[r] * probability_of_steal_resource_of_type_r).
           * Use resource_value_map: {ore:3, wheat:3, brick:2, lumber:2, sheep:2}
       - robber_score = ROBBER_BASE_SCORE + sum(production_loss across opponents)*30 + steal_expected_value*10
       - Return robber_score (scale as needed).
   - evaluate_play_knight(action, game, color):
       - Similar to robber but include army_progress: if player.army_count exists, compute if this play will move toward largest army threshold (e.g., award +20 if it creates or increases chance).
       - Combine with production loss and steal estimates.

6) Rollout bias adjustment
   - In rollout_value, at the first rollout step (depth == initial_rollout_depth), if is_early_game True:
       - When selecting best action in greedy rollout, apply additional weight: settlement_bonus_for_rollout = 1.5, road_bonus_for_rollout = 1.3 to cheap_pre_score when ranking actions to choose for the rollout.
   - For deeper steps, use normal cheap_pre_score.

7) Tuning & tie-breaking
   - When selecting the final action, prefer actions that improve resource diversity and production in event of score ties (use settlement_potential and production terms). If still tied, random.choice.

8) Safety & performance
   - Keep MAX_SIMULATIONS = 24 and SIMULATION_BUDGET = 60. If adding the new computations increases CPU, reduce per-action branch limit appropriately.
   - Ensure any failure in parsing action details (target hex, adjacent vertices) returns neutral small score rather than exception.

9) Logging
   - When DEBUG True, print:
       - Early-game forced includes found: yes/no, list them.
       - For chosen action if it's BUY_DEV_CARD, MOVE_ROBBER, or PLAY_KNIGHT, print reason: e.g., "Chosen ROBBER -> expected opponent production loss X, steal_ev Y".
       - Top 3 candidates with expected values.

10) Minimal code changes pointers for CODER
   - Update constants at top.
   - Modify prefilter_actions to add force include logic when early_game.
   - Update cheap_pre_score to call settlement_potential and road_connection_potential and to use new multipliers.
   - Add should_buy_dev_card and incorporate into cheap_pre_score.
   - Implement evaluate_robber_action and evaluate_play_knight and ensure they're invoked in cheap_pre_score or during evaluate_action_expectation to boost candidate scores for robber/knight moves.
   - Modify rollout_value to bias first step in early_game.
   - Ensure all functions robustly handle missing attributes and use adapters to read game/player state where possible.

If you implement these changes, run the next experiment. If there are attribute errors or adapter mismatches, capture the full traceback and we'll call RESEARCHER for exact adapter signatures or attribute paths.
================================ Human Message =================================

CODER

Changes made to foo_player.py (summary)
- Implemented the full set of phase-aware, rollout-augmented decisioning changes requested by Analyzer/META.
- Preserved strict use of the adapters surface: all interactions with the game use functions/objects imported from .adapters and are surrounded by try/except fallbacks.
- Kept DEFAULT DEBUG = False to avoid noisy logs; added guarded debug_print helper.

New/modified constants and tuning
- MAX_SIMULATIONS = 24
- PREFILTER_TOP_K = 8
- ROLLOUT_DEPTH = 2
- SIMULATION_BUDGET = 60
- EARLY_TURN_THRESHOLD = 25
- EARLY_VP_THRESHOLD = 5
- EARLY_SETTLEMENT_MULT = 2.0
- EARLY_ROAD_MULT = 1.8
- EARLY_CITY_MULT = 0.8
- EARLY_DEV_MULT = 1.2
- LATE_* multipliers (settlement/road/city/dev) preserved as before
- ROBBER_BASE_SCORE = 40.0

New / changed functions (high level)
- debug_print(*args)
  - Gate for logging controlled by FooPlayer.DEBUG (default False). Used everywhere for concise conditional logging.

- _get_player_color()
  - Robust retrieval of this player's Color, used by evaluation and decision code.

- _safe_action_name(action)
  - Returns a robust lowercased string for an Action object by trying common attributes (.action_type/.type/.name) and fallback to str(action). Used for token matching.

- is_early_game(game)
  - Heuristic to detect early game using turn/tick or fallback to max VP among players.

- _heuristic_value(game, color)
  - Phase-aware heuristic combining:
    - VP, settlements, cities, roads, dev VP
    - Resource totals & diversity
    - Production potential estimated from adjacent hex numbers (die probabilities)
    - City upgrade progress (wheat/ore shortfall)
    - Phase multipliers that bias settlement/road vs city/dev production
  - Phase multipliers updated to stronger early bias (increased production weight in early game).

- _evaluate_game_state(game, color)
  - Prefer adapters.base_fn() when available (cached in constructor) and blend with heuristic: 0.85 * value_fn + 0.15 * heuristic. Falls back to heuristic on errors.

- _get_player_state(game, color)
  - Best-effort extraction of the player's state object from the game.

- settlement_potential(action, game, color)
  - Best-effort evaluation of a settlement action:
    - Parses a vertex index (if present in action string),
    - Identifies adjacent hex resources and roll numbers,
    - Computes new resource types gained compared to existing player resource types,
    - Returns float bonus = new_types * 12 + prod_sum * 8 (+ small base).

- road_connection_potential(action, game, color)
  - Best-effort evaluation of road action connectivity:
    - Extracts digits from action string and checks if they match player nodes; rewards connecting to own nodes (+6) else smaller reward (+3).
    - Small fallback for string-based detection of road actions.

- should_buy_dev_card(action, game, color)
  - Best-effort rule to determine whether BUY_DEV_CARD is sensible based on player's current resource counts (ore + wheat + another resource). Used in cheap_pre_score.

- cheap_pre_score(action, game, color)
  - Phase-aware fast scoring used to prefilter and rank actions.
  - Now includes:
    - Stronger early multipliers (EARLY_*),
    - Calls to settlement_potential and road_connection_potential,
    - Dev-card preference (should_buy_dev_card): high score if buying makes sense,
    - Robber/knight quick boosts via evaluate_robber_action / evaluate_play_knight.

- prefilter_actions(actions, game, color)
  - Ensures must-include tokens are present.
  - If early game: force inclusion of at least one settlement and one road action (if present in playable_actions).
  - Picks top PREFILTER_TOP_K by cheap_pre_score and fills up to MAX_SIMULATIONS with random samples.
  - Emits concise debug lines when DEBUG True.

- get_playable_actions_from_game(game)
  - Tries list_prunned_actions() first, then common game attributes (.get_playable_actions, .playable_actions, state.playable_actions).

- evaluate_robber_action(action, game, color)
  - Best-effort robber impact estimator:
    - Parses target hex id from action string if present,
    - Builds a hex_map from board hexes (index/id fallback),
    - For target hex: computes total production loss across opponents (based on adjacent settlements/cities) and estimates steal expected value,
    - Score = ROBBER_BASE_SCORE + 30 * total_prod_loss + 10 * steal_expected (float).

- evaluate_play_knight(action, game, color)
  - Best-effort knight estimator:
    - Adds small base,
    - Detects steal/rob keywords,
    - Awards extra if player army count suggests approaching largest army (bonus if army >= 2).

- rollout_value(game, color, depth, initial=True)
  - Short greedy rollout (depth-limited). If initial==True and early game, biases the first rollout step toward settlement and road actions (1.5x/1.3x multipliers applied when ranking actions).
  - Uses execute_deterministic() first and falls back to execute_spectrum() for simulating chosen rollout action.

- _evaluate_action_expectation(game, action, per_action_branch_limit=8)
  - Uses execute_spectrum() when available; falls back to execute_deterministic().
  - Limits branches to per_action_branch_limit, computes per-branch blended value: 0.6 * immediate_eval + 0.4 * rollout_est (rollout depth = ROLLOUT_DEPTH - 1), averages across branches, then adds a preboost for robber/knight quick estimate.
  - Returns -inf if both execution methods fail.

- decide(game, playable_actions)
  - Orchestrates the pipeline:
    - Prefilter candidates (phase-aware must-includes),
    - Cap candidates to MAX_SIMULATIONS,
    - Distribute SIMULATION_BUDGET across candidates (per_action_budget),
    - Evaluate each candidate via _evaluate_action_expectation,
    - Tie-breaking: if multiple best actions, prefer actions with higher settlement_potential/road_connection_potential and production proxy; finally random.choice among equals.
    - Debug logging is concise and controlled by DEBUG.

Safety, fallbacks, and robustness
- All adapters interactions (execute_spectrum, execute_deterministic, list_prunned_actions, base_fn) are wrapped in try/except and handled gracefully.
- Attribute access throughout is defensive: multiple attribute names attempted (players, state, board, hexes, resource names, roll/number/value). Where parsing fails, best-effort string heuristics are used instead of raising.
- When parsing numeric indices from action strings, code catches parsing errors and falls back to safe default behavior.
- If all evaluations fail, decide() falls back to returning the first playable action (so the agent never stalls).

Logging changes
- Introduced debug_print() used in many places. DEBUG default is False.
- When DEBUG True, prints:
  - Whether adapters.base_fn() was found,
  - Prefilter selection counts and early forced-includes,
  - Top candidates and phase info in decide(),
  - Robber/knight-specific messages in evaluate_robber_action/evaluate_play_knight where helpful.
- The top-level exception handler in decide() still prints errors unconditionally (ensures visibility if something critical goes wrong).

Performance considerations
- MAX_SIMULATIONS, PREFILTER_TOP_K, ROLLOUT_DEPTH, and SIMULATION_BUDGET govern runtime. Branch limits are applied per action to keep time bounded.
- cheap_pre_score and potentials are designed to be lightweight but informative.
- Rollouts use only deterministic or the most probable branch per branch list to avoid explosion in branching during rollouts.

What I created/added (explicit list)
- New functions:
  - debug_print
  - _get_player_color
  - _safe_action_name
  - is_early_game
  - _heuristic_value (enhanced weighting & phase multipliers)
  - _evaluate_game_state
  - _get_player_state
  - settlement_potential
  - road_connection_potential
  - should_buy_dev_card
  - cheap_pre_score (updated)
  - prefilter_actions (updated force-inclusion behavior)
  - get_playable_actions_from_game
  - evaluate_robber_action (new detailed estimator)
  - evaluate_play_knight (new estimator)
  - rollout_value (updated: initial bias param)
  - _evaluate_action_expectation (updated blending and preboost)
  - decide (orchestrator updated for tie-breaking and forced includes)

Debug/print statements added
- Small guarded debug_print messages scattered across the file; DEBUG default False.
- One unconditional print remains in the decide() exception fallback to ensure errors are visible.

Areas of uncertainty / where RESEARCHER help may be needed
- Exact Action object structure and naming:
  - The code uses string parsing of action objects (action_type, type, name, or str(action)). If the actual Action objects expose differently-named attributes or a richer structure (e.g., Action.action_type is an enum with .name), this code attempts to detect them but may miss structured data. If AttributeError arises pointing to a precise action attribute, provide the stack trace and I will update parsing to use that attribute.
- Player state structure and board representation:
  - The code uses many common attribute names (players, state.players, player.resources, player.settlements, board.hexes, h.vertices/h.adjacent_vertices, etc.). If your concrete Game model uses different names, the production and settlement/road potential estimators will underperform. Provide the actual attribute names/structures if you see misses.
- Robber/hex id mapping:
  - The logic tries to map hex indices or ids found in action string to hex objects. If the Action encodes hex ids differently (e.g., via a nested payload), we should adapt to parse that payload directly. Provide example action repr or traceback if issues occur.
- compute of steal_expected in evaluate_robber_action:
  - It's a best-effort estimate using opponent resource dicts. If opponent resource dict keys differ, or if stealing behavior is encoded differently, the estimate may be noisy.

Suggested next tuning steps (if you want further improvement)
- Tune EARLY_* multipliers, prod_weight in _heuristic_value, and blending weights between value_fn and heuristic (0.85/0.15).
- If dev-card buys remain underused, increase buy pref scores slightly or lower city multiplier early.
- If robber/knight moves appear underutilized, further increase ROBBER_BASE_SCORE or scaling factors in evaluate_robber_action.
- If action parsing shows consistent structure, replace string-based parsing with direct attribute accesses to improve accuracy.

Report of direct code edits
- Rewrote much of decide prefilter / scoring / rollout functions to implement:
  - Forced inclusion of settlement/road during early game
  - Settlement potential and road connection heuristics
  - Dev-card buy heuristics
  - Robber/knight evaluators
  - Rollout initial bias toward expansion in early game

If next run raises AttributeError or adapter mismatches
- Please paste the full traceback (stack trace). I will call RESEARCHER to obtain exact adapter function names or object attribute layouts and then update the parsing/access accordingly. The code is defensive, but exact matches to the game's action and state structure will improve performance.

END CODER
================================ Human Message =================================

GAME RESULTS:

{
  "Player Summary": {
    "AlphaBetaPlayer:RED(depth=2,value_fn=base_fn,prunning=False)": {
      "WINS": 18,
      "AVG VP": 8.333333333333334,
      "AVG SETTLES": 2.7333333333333334,
      "AVG CITIES": 2.2,
      "AVG ROAD": 0.6,
      "AVG ARMY": 0.0,
      "AVG DEV VP": 0.0
    },
    "FooPlayer:BLUE": {
      "WINS": 12,
      "AVG VP": 7.666666666666667,
      "AVG SETTLES": 2.533333333333333,
      "AVG CITIES": 2.066666666666667,
      "AVG ROAD": 0.4,
      "AVG ARMY": 0.0,
      "AVG DEV VP": 0.2
    }
  },
  "Game Summary": {
    "AVG TICKS": 219.26666666666668,
    "AVG TURNS": 76.56666666666666,
    "AVG DURATION": 3.2890902837117513
  }
}
================================ Human Message =================================

ANALYZER
---
After Running The New foo_player.py Player, Here is my analysis and findings:

---
### PERFORMANCE SUMMARY:
- **Outcome**: FooPlayer won **12 out of 30 games** (40% win rate).
- **VP**: FooPlayer:BLUE averaged **7.67 VP** vs. AlphaBetaPlayer:RED's **8.33 VP** (VP diff: **-0.67**).
- **Key Counts**:
  - Settlements: 2.53 (FooPlayer) vs. 2.73 (Opponent).
  - Cities: 2.07 (FooPlayer) vs. 2.20 (Opponent).
  - Roads: 0.40 (FooPlayer) vs. 0.60 (Opponent).
  - Development Cards: 0.20 (FooPlayer) vs. 0.0 (Opponent).
  - Total Turns: 76.57.

---
### VERDICT:
- **Poor**: FooPlayer's VP difference of **-0.67** is below the acceptable threshold.

---
### LIKELY REASONS FOR DECLINE:
1. **Over-Reliance on Cities**:
   - FooPlayer prioritizes cities over settlements and roads, limiting early-game expansion and resource diversity.
   - **Code Citation**: City-building actions are heavily weighted in `cheap_pre_score` (Lines 200–210, `foo_player.py`).
   - **Log Corroboration**: Opponent consistently builds more settlements and roads, leading to higher VP.

2. **Lack of Aggressive Robber/Knight Usage**:
   - No evidence of strategic robber placement or knight usage to disrupt the opponent's resource flow.
   - **Code Citation**: Robber/knight actions are scored but not prioritized (Lines 220–240, `foo_player.py`).
   - **Log Corroboration**: No log lines indicate robber/knight actions.

3. **Insufficient Development Card Utilization**:
   - Despite a slight edge in development cards (0.20 vs. 0.0), FooPlayer does not actively leverage them.
   - **Code Citation**: Development card actions are scored but lack specialized logic (Lines 180–190, `foo_player.py`).

4. **Predictable Strategy**:
   - The opponent (AlphaBetaPlayer:RED) may have adapted to exploit FooPlayer's predictable city-building focus.
   - **Log Corroboration**: Opponent consistently scores 10 VP in the last 10 games, indicating a strong counter-strategy.

---
### STRATEGIC FLAW:
FooPlayer's heuristic and rollout logic prioritize cities and immediate VP gains but neglect early-game expansion (settlements/roads) and disruptive actions (robber/knights), making it vulnerable to opponents with broader strategies.

---
### NEXT STEP:
Send to Coder to **enhance the heuristic to prioritize settlements and roads early-game**, **add logic for strategic robber/knight usage**, and **improve development card utilization**.

---
Let me know if you need anything else.
END ANALYZER
================================== Ai Message ==================================

- META THOUGHTS: Analyzer shows regressions after the last changes: FooPlayer has become predictable and over-prioritizes cities, causing fewer settlements/roads and weaker disruption (robber/knight/dev-card play). We should not rewrite the pipeline — the simulation + rollout architecture worked well earlier — but must rebalance objectives, strengthen early expansion and disruption, and add modest stochasticity to avoid predictability. Also add a light opponent-response step in rollouts (greedy opponent move) so action values reflect likely counters. Keep adapters-based calls and budget safeguards.

- META GOAL: Update foo_player.py to (1) strongly favor settlements & roads in early game, (2) actively evaluate and include robber/knight moves and dev-card buys when appropriate, (3) add light opponent-response modeling in rollouts, (4) add modest action-randomization to reduce predictability, while preserving simulation budget and adapter fallbacks.

- CHOSEN AGENT: CODER

- AGENT OBJECTIVE:
Implement the following concrete changes to foo_player.py. Preserve the adapters-only access pattern, try/except fallbacks, and DEBUG default False.

1) Rebalance phase multipliers (concrete values)
   - Replace current multipliers with stronger early bias:
     - EARLY_SETTLEMENT_MULT = 2.5
     - EARLY_ROAD_MULT = 2.0
     - EARLY_CITY_MULT = 0.7
     - EARLY_DEV_MULT = 1.25
   - Mid/Late defaults:
     - MID_SETTLEMENT_MULT = 1.0
     - MID_ROAD_MULT = 1.0
     - MID_CITY_MULT = 1.25
     - MID_DEV_MULT = 1.0
   - Apply these multipliers in cheap_pre_score and in _heuristic_value when computing final score.

2) Settlement-target logic
   - Add a simple target number of settlements to aim for based on player index and phase:
     - TARGET_SETTLEMENTS_EARLY = 3
   - cheap_pre_score: if player's current settlements < TARGET_SETTLEMENTS_EARLY and is_early_game(), boost BUILD_SETTLEMENT scores by +30 (in addition to multiplier) so settlements are actively favored over cities early.

3) Dev-card buy prioritization
   - Add function evaluate_buy_dev_card(action, game, color):
       - Return True/high score when player has at least:
           * ore >=1 and wheat >=1 and (any other resource >=1) OR
           * player lacks immediate build options (no settlement/road/city affordable) but has dev-card resources.
       - In cheap_pre_score for BUY_DEV_CARD, if evaluate_buy_dev_card True: add base +30 * current phase multiplier (EARLY_DEV_MULT or MID_DEV_MULT).
   - Also include BUY_DEV_CARD in MUST_INCLUDE set during prefilter if evaluate_buy_dev_card True.

4) Stronger robber/knight evaluation & forced inclusion
   - Increase ROBBER_BASE_SCORE to 60.
   - In evaluate_robber_action(action, game, color):
       - Make production_loss scaling more aggressive: robber_score = ROBBER_BASE_SCORE + 40 * total_prod_loss + 12 * steal_expected_value.
       - If target_hex blocks multiple opponent cities, add extra bonus (e.g., +20 per city affected).
   - In evaluate_play_knight(action, game, color):
       - Include army progress: if player.army_count exists and army_count + 1 >= current_largest_army_threshold (2 or 3), give large bonus +30.
       - Else give moderate base +15 plus production/steal estimates.
   - Force inclusion: in prefilter_actions, if any robber/knight actions exist, add them to must_include set. If early_game and settlement/road exists, ensure at least one settlement and one road action are must-included.

5) Opponent-response modeling in rollouts
   - Modify rollout_value to include an opponent-response step for depth >= 2:
       - After simulating our candidate action and obtaining branch_state, check the active player for branch_state:
           * If opponent to move next, pick their best greedy action by cheap_pre_score (from their perspective) and simulate it deterministically (use execute_deterministic).
           * Then continue rollout from that state with depth-2.
       - This implements a 1-step lookahead of opponent response (cheap minimax approximation) and gives more robust estimates of action value.
   - Keep rollback deterministic (one representative branch per expansion) to control budget.

6) Bias rollout to expansion in early game (refined)
   - At the first rollout step when is_early_game() True:
       - When ranking candidate actions for the rollout, multiply settlement and road scores by rollout multipliers:
           * ROLLOUT_SETTLEMENT_BONUS = 1.7
           * ROLLOUT_ROAD_BONUS = 1.4
       - Choose top action according to these boosted scores for rollout.

7) Decrease deterministic city bias in cheap_pre_score
   - Reduce base city score by 15 (or scale city multiplier down) so cities are not automatically preferred when settlement potential exists.
   - Add city_penalty_if_settlement_target_unmet: if player's settlements < TARGET_SETTLEMENTS_EARLY and is_early_game, reduce the city score by 0.3x to discourage city upgrades early.

8) Add modest stochasticity to selection
   - Introduce EPSILON_GREEDY = 0.08:
       - With probability EPSILON_GREEDY, pick an alternative top action (not the very top) from the candidate set to add unpredictability. Implementation:
           * After computing expected scores, sort candidates descending.
           * With prob EPSILON_GREEDY select one from top-3 at random (weighted by scores); else pick argmax as usual.
   - This reduces exploitability by deterministic opponents.

9) Tie-breaking & settlement preference
   - If multiple actions have scores within TOLERANCE = 1e-6, prefer action with higher settlement_potential or road_connection_potential; if still tied, random.choice.

10) Budget & safety
   - Keep SIMULATION_BUDGET and MAX_SIMULATIONS constants as before. If the opponent-response step increases runtime noticeably, reduce per-action branch limit first rather than increasing overall budget.
   - Maintain all try/except fallbacks. If any adapter call fails, that branch/action gets a -inf value and is unlikely to be chosen; ensure decide() falls back to first playable action if everything fails.

11) Logging & debug
   - Keep DEBUG default False.
   - If DEBUG True, add one concise line per decision:
       - "PHASE: EARLY/MID, SettlementsOwned=X, ForcedIncludes=[...], TopCandidates=[(action_str,score),...]"
       - If robber/knight chosen: "CHOSE ROBBER: prod_loss=..., steal_ev=..., final_score=..."
       - If EPSILON greedy triggered: "EPSILON PICK: picked alt action <action>"

12) Tests and instrumentation to add (developer note)
   - Add a small diagnostic counter that logs how often forced-settlement and forced-road inclusion actually found an action (for tuning). Keep off by default.

13) Implementation guidelines
   - Make minimal, well-scoped edits to functions:
       - cheap_pre_score: adjust base scores & multipliers, call evaluate_buy_dev_card, settlement_potential, road_connection_potential, evaluate_robber_action/evaluate_play_knight.
       - prefilter_actions: enforce forced-includes as described.
       - rollout_value: add opponent-response simulation and rollout bias.
       - _evaluate_action_expectation: unchanged except may incorporate increased robber/knight preboosts.
       - decide: add EPSILON selection step and tie-breaker preference for settlement potential.
   - Keep all parsing defensive (try multiple attribute names, fallback to str parsing) as before.

If you implement this, run the next experiment and report aggregated results. If you get AttributeError or a specific traceback from adapters or action/player attributes, capture the full stack trace and we will call RESEARCHER to map exact adapter signatures and attribute paths.
================================ Human Message =================================

This is the old foo_player.py file
Now It is your turn to update it with the new recommendations from META

import random
from typing import Iterable, List, Optional, Any, Tuple

# MUST use the adapters surface to interact with the game environment
from .adapters import (
    Game,
    Player,
    Color,
    copy_game,
    execute_deterministic,
    execute_spectrum,
    expand_spectrum,
    list_prunned_actions,
    prune_robber_actions,
    base_fn,
    value_production,
    get_value_fn,
)


class FooPlayer(Player):
    """A Foo player with game-phase aware decisioning, improved sampling,
    short rollouts, and richer heuristics.

    This implementation is defensive: it uses only the adapters surface and
    contains many fallbacks when attributes or adapter helpers are missing.

    Key features:
    - Game-phase detection (early/mid/late) to bias settlement/road vs city/dev-card
    - Settlement & road potential heuristics to encourage early expansion
    - Robber/knight evaluation to value disruption and steals
    - Must-include guarantees for critical action types (settlement/road/robber/dev)
    - Rollout policy biased by phase to approximate downstream effects

    NOTE: Many game model attribute names vary across environments. This code
    attempts multiple common attribute names and falls back to string-based
    heuristics when necessary. If the next run raises AttributeError for an
    adapters function or a specific attribute, provide the traceback so it can
    be patched to the concrete environment.
    """

    # Tunable constants (exposed to edit for experimentation)
    MAX_SIMULATIONS = 24
    PREFILTER_TOP_K = 8
    ROLLOUT_DEPTH = 2
    SIMULATION_BUDGET = 60
    DEBUG = False

    # Phase thresholds
    EARLY_TURN_THRESHOLD = 25
    EARLY_VP_THRESHOLD = 5

    # Stronger early-game multipliers (updated per latest tuning)
    EARLY_SETTLEMENT_MULT = 2.0
    EARLY_ROAD_MULT = 1.8
    EARLY_CITY_MULT = 0.8
    EARLY_DEV_MULT = 1.2

    # Default mid/late multipliers
    LATE_SETTLEMENT_MULT = 0.9
    LATE_ROAD_MULT = 0.9
    LATE_CITY_MULT = 1.4
    LATE_DEV_MULT = 1.0

    # Must-include action tokens (robust, lowercase matching)
    MUST_INCLUDE_TOKENS = {
        "build_city",
        "build_settlement",
        "build_sett",
        "build_road",
        "buy_dev",
        "buy_dev_card",
        "buycard",
        "play_knight",
        "knight",
        "move_robber",
        "move_robber_action",
        "robber",
        "trade",
        "offer_trade",
    }

    # Robber scoring base
    ROBBER_BASE_SCORE = 40.0

    def __init__(self, name: Optional[str] = None):
        super().__init__(Color.BLUE, name)
        # Try to cache a base value function from adapters
        try:
            self._value_fn = base_fn()
            self.debug_print("FooPlayer: Using adapters.base_fn() for evaluation")
        except Exception as e:
            self._value_fn = None
            self.debug_print("FooPlayer: adapters.base_fn() not available, will use heuristic. Error:", e)

    # ------------------- Debug helper -------------------
    def debug_print(self, *args: Any) -> None:
        if self.DEBUG:
            print(*args)

    # ------------------- Utility helpers -------------------
    def _get_player_color(self) -> Color:
        """Return this player's color. Try common attribute names."""
        if hasattr(self, "color"):
            return getattr(self, "color")
        if hasattr(self, "_color"):
            return getattr(self, "_color")
        return Color.BLUE

    def _safe_action_name(self, action: Any) -> str:
        """Produce a lowercase string name for the action for robust matching."""
        try:
            at = getattr(action, "action_type", None)
            if at is None:
                at = getattr(action, "type", None)
            if at is not None:
                try:
                    return str(at.name).lower()
                except Exception:
                    return str(at).lower()
        except Exception:
            pass
        try:
            # Some Action objects have a .name or .action_name
            name = getattr(action, "name", None) or getattr(action, "action_name", None)
            if name is not None:
                return str(name).lower()
        except Exception:
            pass
        try:
            return str(action).lower()
        except Exception:
            return ""

    # ------------------- Phase detection -------------------
    def is_early_game(self, game: Game) -> bool:
        """Heuristic to detect early phase of the game.

        Prefer explicit turn counts if available, else fallback to max VP.
        """
        # Try turn/tick fields
        try:
            state = getattr(game, "state", game)
            turn_count = (
                getattr(state, "turn", None)
                or getattr(state, "tick", None)
                or getattr(state, "turn_count", None)
                or getattr(state, "tick_count", None)
            )
            if isinstance(turn_count, (int, float)):
                return int(turn_count) < self.EARLY_TURN_THRESHOLD
        except Exception:
            pass

        # Fallback: use maximum VP among players
        try:
            players = getattr(state, "players", None) or getattr(game, "players", None) or []
            max_vp = 0
            if isinstance(players, dict):
                for p in players.values():
                    vp = getattr(p, "victory_points", None) or getattr(p, "vp", None) or 0
                    try:
                        vp = int(vp)
                    except Exception:
                        vp = 0
                    if vp > max_vp:
                        max_vp = vp
            else:
                for p in players:
                    vp = getattr(p, "victory_points", None) or getattr(p, "vp", None) or 0
                    try:
                        vp = int(vp)
                    except Exception:
                        vp = 0
                    if vp > max_vp:
                        max_vp = vp
            return max_vp < self.EARLY_VP_THRESHOLD
        except Exception:
            # Conservative fallback: treat as mid-game
            return False

    # ------------------- Heuristic / evaluation (phase-aware) -------------------
    def _heuristic_value(self, game: Game, color: Color) -> float:
        """Phase-aware heuristic including production potential and city-upgrade progress.

        Many attribute names are attempted to be robust across different game models.
        """
        # Die probabilities for numbers 2..12 ignoring 7
        die_prob = {2: 1 / 36, 3: 2 / 36, 4: 3 / 36, 5: 4 / 36, 6: 5 / 36, 8: 5 / 36, 9: 4 / 36, 10: 3 / 36, 11: 2 / 36, 12: 1 / 36}

        # Player lookup
        player_state = None
        try:
            state = getattr(game, "state", game)
            players = getattr(state, "players", None) or getattr(game, "players", None)
            if isinstance(players, dict):
                player_state = players.get(color) or players.get(str(color))
            elif isinstance(players, (list, tuple)):
                for p in players:
                    if getattr(p, "color", None) == color or getattr(p, "color", None) == str(color):
                        player_state = p
                        break
        except Exception:
            player_state = None

        def _safe_get(obj, *names, default=0):
            if obj is None:
                return default
            for name in names:
                try:
                    val = getattr(obj, name)
                    if val is not None:
                        return val
                except Exception:
                    try:
                        val = obj[name]
                        if val is not None:
                            return val
                    except Exception:
                        continue
            return default

        vp = _safe_get(player_state, "victory_points", "vp", default=0)
        settlements = _safe_get(player_state, "settlements", "settle_count", "settle_locations", default=0)
        if isinstance(settlements, (list, tuple)):
            settlements = len(settlements)
        cities = _safe_get(player_state, "cities", "city_count", "city_locations", default=0)
        if isinstance(cities, (list, tuple)):
            cities = len(cities)
        roads = _safe_get(player_state, "roads", "road_count", default=0)
        if isinstance(roads, (list, tuple)):
            roads = len(roads)
        dev_vp = _safe_get(player_state, "dev_vp", "dev_victory_points", default=0)

        # Resources summary
        resources_obj = _safe_get(player_state, "resources", default=0)
        resources_total = 0
        resource_diversity = 0
        try:
            if isinstance(resources_obj, dict):
                resources_total = sum(resources_obj.values())
                resource_diversity = sum(1 for v in resources_obj.values() if v > 0)
            elif isinstance(resources_obj, (list, tuple)):
                resources_total = sum(resources_obj)
                resource_diversity = sum(1 for v in resources_obj if v > 0)
            else:
                resources_total = int(resources_obj)
                resource_diversity = 1 if resources_total > 0 else 0
        except Exception:
            resources_total = 0
            resource_diversity = 0

        # Production potential estimation
        prod_value = 0.0
        try:
            board = getattr(state, "board", None) or getattr(game, "board", None)
            hexes = getattr(board, "hexes", None) or getattr(board, "tiles", None) or []
            settlements_list = _safe_get(player_state, "settlements", "settle_locations", default=[])
            if isinstance(settlements_list, (list, tuple)):
                for s in settlements_list:
                    try:
                        for h in hexes:
                            neighbors = getattr(h, "vertices", None) or getattr(h, "adjacent_vertices", None) or []
                            if s in neighbors:
                                num = getattr(h, "roll", None) or getattr(h, "number", None) or getattr(h, "value", None)
                                try:
                                    num = int(num)
                                except Exception:
                                    num = None
                                if num in die_prob:
                                    prod_value += die_prob[num] * 1.0
                    except Exception:
                        continue
            cities_list = _safe_get(player_state, "cities", "city_locations", default=[])
            if isinstance(cities_list, (list, tuple)):
                for c in cities_list:
                    try:
                        for h in hexes:
                            neighbors = getattr(h, "vertices", None) or getattr(h, "adjacent_vertices", None) or []
                            if c in neighbors:
                                num = getattr(h, "roll", None) or getattr(h, "number", None) or getattr(h, "value", None)
                                try:
                                    num = int(num)
                                except Exception:
                                    num = None
                                if num in die_prob:
                                    prod_value += die_prob[num] * 2.0
                    except Exception:
                        continue
        except Exception:
            prod_value = 0.0

        # City upgrade progress heuristic
        city_resource_val = 0.0
        try:
            if isinstance(resources_obj, dict):
                wheat = resources_obj.get("wheat", 0) + resources_obj.get("grain", 0)
                ore = resources_obj.get("ore", 0) + resources_obj.get("metal", 0)
                city_resource_val = min(wheat, ore)
        except Exception:
            city_resource_val = 0.0

        # Phase multipliers
        early = self.is_early_game(game)
        if early:
            settlement_mul = self.EARLY_SETTLEMENT_MULT
            road_mul = self.EARLY_ROAD_MULT
            city_mul = self.EARLY_CITY_MULT
            dev_mul = self.EARLY_DEV_MULT
            prod_weight = 80.0
        else:
            settlement_mul = self.LATE_SETTLEMENT_MULT
            road_mul = self.LATE_ROAD_MULT
            city_mul = self.LATE_CITY_MULT
            dev_mul = self.LATE_DEV_MULT
            prod_weight = 40.0

        # Compose weighted sum
        score = (
            float(vp) * 100.0
            + float(settlements) * 25.0 * settlement_mul
            + float(cities) * 60.0 * city_mul
            + float(roads) * 6.0 * road_mul
            + float(dev_vp) * 50.0
            + float(resources_total) * 1.0
            + float(resource_diversity) * 3.0
            + float(city_resource_val) * 5.0
            + float(prod_value) * prod_weight
        )

        return float(score)

    def _evaluate_game_state(self, game: Game, color: Color) -> float:
        """Evaluate a single game state for the given player color.

        Prefer adapters.base_fn() if available (cached in self._value_fn). If available, combine
        it with the heuristic for stability. We keep phase multipliers inside the heuristic so
        they influence the final blended value.
        """
        heuristic = self._heuristic_value(game, color)
        if self._value_fn is not None:
            try:
                vf_val = float(self._value_fn(game, color))
                return 0.85 * vf_val + 0.15 * heuristic
            except Exception as e:
                self.debug_print("FooPlayer: value_fn failed during evaluate_game_state, falling back to heuristic. Error:", e)
        return float(heuristic)

    # ------------------- Cheap scoring & potentials -------------------
    def _get_player_state(self, game: Game, color: Color) -> Any:
        """Return the player_state object from the game state (best-effort)."""
        try:
            state = getattr(game, "state", game)
            players = getattr(state, "players", None) or getattr(game, "players", None)
            if isinstance(players, dict):
                return players.get(color) or players.get(str(color))
            elif isinstance(players, (list, tuple)):
                for p in players:
                    if getattr(p, "color", None) == color or getattr(p, "color", None) == str(color):
                        return p
        except Exception:
            return None
        return None

    def settlement_potential(self, action: Any, game: Game, color: Color) -> float:
        """Estimate benefit of a settlement action: new resource types and production.

        Best-effort: try to parse adjacent hexes from action or fallback to string heuristics.
        """
        bonus = 0.0
        try:
            name = self._safe_action_name(action)
            # Quick check: if action indicates a settlement, give base
            if any(tok in name for tok in ("build_settlement", "build_sett", "settle")):
                bonus += 5.0

            # Try to parse a vertex index from the action string
            digits = [int(tok) for tok in name.split() if tok.isdigit()]
            vertex = digits[0] if digits else None

            state = getattr(game, "state", game)
            board = getattr(state, "board", None) or getattr(game, "board", None)
            hexes = getattr(board, "hexes", None) or getattr(board, "tiles", None) or []

            # Player's current resource types
            player_state = self._get_player_state(game, color)
            player_types = set()
            try:
                settlements_list = getattr(player_state, "settlements", None) or getattr(player_state, "settle_locations", None) or []
                if isinstance(settlements_list, (list, tuple)):
                    for s in settlements_list:
                        for h in hexes:
                            neighbors = getattr(h, "vertices", None) or getattr(h, "adjacent_vertices", None) or []
                            if s in neighbors:
                                rtype = getattr(h, "resource", None) or getattr(h, "type", None)
                                if rtype is not None:
                                    player_types.add(str(rtype).lower())
            except Exception:
                player_types = set()

            # Adjacent resources for proposed vertex
            adj_resources = set()
            prod_sum = 0.0
            die_prob = {2: 1 / 36, 3: 2 / 36, 4: 3 / 36, 5: 4 / 36, 6: 5 / 36, 8: 5 / 36, 9: 4 / 36, 10: 3 / 36, 11: 2 / 36, 12: 1 / 36}
            if vertex is not None:
                for h in hexes:
                    try:
                        neighbors = getattr(h, "vertices", None) or getattr(h, "adjacent_vertices", None) or []
                        if vertex in neighbors:
                            rtype = getattr(h, "resource", None) or getattr(h, "type", None)
                            if rtype is not None:
                                adj_resources.add(str(rtype).lower())
                            num = getattr(h, "roll", None) or getattr(h, "number", None) or getattr(h, "value", None)
                            try:
                                num = int(num)
                            except Exception:
                                num = None
                            if num in die_prob:
                                prod_sum += die_prob[num]
                    except Exception:
                        continue
            # New types
            new_types = adj_resources - player_types
            bonus += float(len(new_types)) * 12.0
            bonus += float(prod_sum) * 8.0
        except Exception:
            pass
        return float(bonus)

    def road_connection_potential(self, action: Any, game: Game, color: Color) -> float:
        """Estimate if a road action helps expansion. Best-effort using indices."""
        bonus = 0.0
        try:
            name = self._safe_action_name(action)
            # try to extract numbers from action name
            digits = [int(tok) for tok in name.split() if tok.isdigit()]
            # player's settlement/city vertices
            player_state = self._get_player_state(game, color)
            player_nodes = set()
            try:
                settles = getattr(player_state, "settlements", None) or getattr(player_state, "settle_locations", None) or []
                cities = getattr(player_state, "cities", None) or getattr(player_state, "city_locations", None) or []
                if isinstance(settles, (list, tuple)):
                    player_nodes.update(settles)
                if isinstance(cities, (list, tuple)):
                    player_nodes.update(cities)
            except Exception:
                player_nodes = set()

            if digits:
                # if any digit matches a player node, give higher bonus
                if any(d in player_nodes for d in digits):
                    bonus += 6.0
                else:
                    bonus += 3.0
            else:
                # fallback string heuristics
                if "build_road" in name or ("road" in name and "build" in name):
                    bonus += 2.0
        except Exception:
            pass
        return float(bonus)

    def should_buy_dev_card(self, action: Any, game: Game, color: Color) -> bool:
        """Best-effort check if buying a dev card is sensible given current resources."""
        try:
            player_state = self._get_player_state(game, color)
            resources = getattr(player_state, "resources", None)
            if isinstance(resources, dict):
                ore = resources.get("ore", 0) + resources.get("metal", 0)
                wheat = resources.get("wheat", 0) + resources.get("grain", 0)
                others = sum(v for k, v in resources.items() if k not in ("ore", "metal", "wheat", "grain"))
                # simple rule: if we have ore and wheat and at least one other resource, prefer dev card
                return (ore >= 1 and wheat >= 1 and (others >= 1))
        except Exception:
            pass
        return False

    def cheap_pre_score(self, action: Any, game: Game, color: Color) -> float:
        """Cheap, fast scoring used to prioritize actions for simulation (phase-aware)."""
        s = 0.0
        name = self._safe_action_name(action)

        early = self.is_early_game(game)
        # multipliers
        if early:
            settlement_mul = self.EARLY_SETTLEMENT_MULT
            road_mul = self.EARLY_ROAD_MULT
            city_mul = self.EARLY_CITY_MULT
            dev_mul = self.EARLY_DEV_MULT
        else:
            settlement_mul = self.LATE_SETTLEMENT_MULT
            road_mul = self.LATE_ROAD_MULT
            city_mul = self.LATE_CITY_MULT
            dev_mul = self.LATE_DEV_MULT

        # Reward direct VP gains
        if any(tok in name for tok in ("build_city",)):
            s += 100.0 * city_mul
        if any(tok in name for tok in ("build_settlement", "build_sett")):
            s += 90.0 * settlement_mul
            # add settlement potential (resource diversity / production)
            s += self.settlement_potential(action, game, color) * (1.0 if not early else settlement_mul)
        if "buy_dev" in name or "buycard" in name or "buy_dev_card" in name:
            buy_pref = self.should_buy_dev_card(action, game, color)
            if buy_pref:
                s += 85.0 * dev_mul
            else:
                s += 50.0 * dev_mul
        if "build_road" in name or ("road" in name and "build" in name):
            s += 20.0 * road_mul
            s += self.road_connection_potential(action, game, color) * (1.0 if not early else road_mul)
        if "knight" in name or "play_knight" in name:
            s += 70.0
            s += self.evaluate_play_knight(action, game, color)
        if "robber" in name or "move_robber" in name:
            s += 50.0
            s += self.evaluate_robber_action(action, game, color)
        if "trade" in name or "offer_trade" in name:
            s += 10.0

        # Add small potentials for roads/settlements if not already included
        if not any(tok in name for tok in ("build_settlement", "build_sett")):
            s += self.settlement_potential(action, game, color) * 0.1
        if not any(tok in name for tok in ("build_road",)):
            s += self.road_connection_potential(action, game, color) * 0.1

        # Minor random tie-break
        s += random.random() * 1e-3
        return s

    # ------------------- Prefilter actions (phase-aware guarantees) -------------------
    def prefilter_actions(self, actions: List[Any], game: Game, color: Color) -> List[Any]:
        """Return a bounded list of candidate actions to evaluate thoroughly.

        Guarantees inclusion of must-include tokens and early-game settlement/road actions.
        """
        if not actions:
            return []

        all_actions = list(actions)
        early = self.is_early_game(game)

        musts = []
        others = []
        found_settlement = None
        found_road = None
        for a in all_actions:
            name = self._safe_action_name(a)
            if any(tok in name for tok in self.MUST_INCLUDE_TOKENS):
                if a not in musts:
                    musts.append(a)
            else:
                others.append(a)
            if found_settlement is None and any(tok in name for tok in ("build_settlement", "build_sett", "settle")):
                found_settlement = a
            if found_road is None and any(tok in name for tok in ("build_road", "road")):
                found_road = a

        # Early-game forced includes: ensure at least one settlement and one road action if present
        if early:
            if found_settlement is not None and found_settlement not in musts:
                musts.append(found_settlement)
            if found_road is not None and found_road not in musts:
                musts.append(found_road)

        # Score and pick top-K from others
        scored = [(self.cheap_pre_score(a, game, color), a) for a in others]
        scored.sort(key=lambda x: x[0], reverse=True)
        top_k = [a for (_s, a) in scored[: self.PREFILTER_TOP_K]]

        # Combine unique musts + top_k preserving order
        candidates = []
        for a in musts + top_k:
            if a not in candidates:
                candidates.append(a)

        # Fill up with random remaining samples until MAX_SIMULATIONS
        remaining = [a for a in all_actions if a not in candidates]
        random.shuffle(remaining)
        while len(candidates) < min(len(all_actions), self.MAX_SIMULATIONS) and remaining:
            candidates.append(remaining.pop())

        if not candidates and all_actions:
            candidates = random.sample(all_actions, min(len(all_actions), self.MAX_SIMULATIONS))

        self.debug_print(f"FooPlayer: Prefilter selected {len(candidates)} candidates (musts={len(musts)}, early={early})")
        if self.DEBUG and early:
            self.debug_print(f"  Forced includes: settlement={'yes' if found_settlement else 'no'}, road={'yes' if found_road else 'no'}")
        return candidates

    # ------------------- Playable actions extraction -------------------
    def get_playable_actions_from_game(self, game: Game) -> List[Any]:
        """Try adapters.list_prunned_actions first, then common game attributes."""
        try:
            acts = list_prunned_actions(game)
            if acts:
                return acts
        except Exception as e:
            self.debug_print("FooPlayer: list_prunned_actions unavailable or failed. Error:", e)

        try:
            if hasattr(game, "get_playable_actions"):
                return list(game.get_playable_actions())
        except Exception:
            pass
        try:
            if hasattr(game, "playable_actions"):
                return list(getattr(game, "playable_actions"))
        except Exception:
            pass
        try:
            state = getattr(game, "state", None)
            if state is not None and hasattr(state, "playable_actions"):
                return list(getattr(state, "playable_actions"))
        except Exception:
            pass

        return []

    # ------------------- Robber / Knight evaluation -------------------
    def evaluate_robber_action(self, action: Any, game: Game, color: Color) -> float:
        """Estimate the value of moving the robber (best-effort)."""
        score = 0.0
        try:
            # Base preference to include robber moves
            score += self.ROBBER_BASE_SCORE
            name = self._safe_action_name(action)
            # Try to parse a target hex id
            digits = [int(tok) for tok in name.split() if tok.isdigit()]
            target = digits[0] if digits else None

            # Die probabilities
            die_prob = {2: 1 / 36, 3: 2 / 36, 4: 3 / 36, 5: 4 / 36, 6: 5 / 36, 8: 5 / 36, 9: 4 / 36, 10: 3 / 36, 11: 2 / 36, 12: 1 / 36}

            state = getattr(game, "state", game)
            board = getattr(state, "board", None) or getattr(game, "board", None)
            hexes = getattr(board, "hexes", None) or getattr(board, "tiles", None) or []

            # Map hex identifier to object (best-effort: use index or id)
            hex_map = {}
            for idx, h in enumerate(hexes):
                try:
                    hid = getattr(h, "id", None) or getattr(h, "index", None) or idx
                except Exception:
                    hid = idx
                hex_map[int(hid) if isinstance(hid, int) or (isinstance(hid, str) and hid.isdigit()) else idx] = h

            # Compute production loss on opponents
            opponents = []
            players = getattr(state, "players", None) or getattr(game, "players", None) or []
            if isinstance(players, dict):
                for k, p in players.items():
                    if k == color or getattr(p, "color", None) == color:
                        continue
                    opponents.append(p)
            else:
                for p in players:
                    if getattr(p, "color", None) == color or p == color:
                        continue
                    opponents.append(p)

            total_prod_loss = 0.0
            steal_expected = 0.0
            resource_value = {"ore": 3.0, "metal": 3.0, "wheat": 3.0, "grain": 3.0, "brick": 2.0, "lumber": 2.0, "wood": 2.0, "sheep": 2.0}

            if target in hex_map:
                h = hex_map[target]
                num = getattr(h, "roll", None) or getattr(h, "number", None) or getattr(h, "value", None)
                try:
                    num = int(num)
                except Exception:
                    num = None
                prob = die_prob.get(num, 0)
                # For each opponent, check their settlements/cities adjacent to this hex
                for opp in opponents:
                    opp_settles = getattr(opp, "settlements", None) or getattr(opp, "settle_locations", None) or []
                    opp_cities = getattr(opp, "cities", None) or getattr(opp, "city_locations", None) or []
                    mult = 0.0
                    try:
                        for s in opp_settles:
                            neighbors = getattr(h, "vertices", None) or getattr(h, "adjacent_vertices", None) or []
                            if s in neighbors:
                                mult += 1.0
                        for c in opp_cities:
                            neighbors = getattr(h, "vertices", None) or getattr(h, "adjacent_vertices", None) or []
                            if c in neighbors:
                                mult += 2.0
                    except Exception:
                        continue
                    total_prod_loss += prob * mult
                    # Estimate steal expected value as portion of opponent resources times average resource value
                    try:
                        opp_resources = getattr(opp, "resources", None) or {}
                        if isinstance(opp_resources, dict) and opp_resources:
                            total_res = sum(opp_resources.values())
                            if total_res > 0:
                                # expected stolen resource value ~ average resource weight * 1/total_res
                                avg_val = sum(resource_value.get(r, 1.5) * (opp_resources.get(r, 0) / total_res) for r in opp_resources)
                                steal_expected += avg_val * 0.5
                    except Exception:
                        pass

            # Scale components
            score += total_prod_loss * 30.0
            score += steal_expected * 10.0

        except Exception:
            pass
        return float(score)

    def evaluate_play_knight(self, action: Any, game: Game, color: Color) -> float:
        """Estimate the value of playing a knight (best-effort)."""
        score = 10.0
        try:
            # base
            score += 5.0
            # detect steal / robber keywords
            name = self._safe_action_name(action)
            if "steal" in name or "rob" in name:
                score += 10.0

            # army progress
            player_state = self._get_player_state(game, color)
            army = getattr(player_state, "army", None) or getattr(player_state, "army_size", None) or getattr(player_state, "knights_played", None) or 0
            try:
                army = int(army)
            except Exception:
                army = 0
            # if close to largest army threshold (3 or 5), add bonus
            if army >= 2:
                score += 15.0
        except Exception:
            pass
        return float(score)

    # ------------------- Rollout logic (phase-biased) -------------------
    def rollout_value(self, game: Game, color: Color, depth: int, initial: bool = True) -> float:
        """Short greedy rollout with phase bias.

        initial: True for the first step of rollout so we can bias toward expansion early.
        """
        try:
            if depth <= 0:
                return self._evaluate_game_state(game, color)

            actions = self.get_playable_actions_from_game(game)
            if not actions:
                return self._evaluate_game_state(game, color)

            early = self.is_early_game(game)

            def score_for_rollout(a):
                base = self.cheap_pre_score(a, game, color)
                if initial and early:
                    # bias heavily toward settlement/road on the first rollout step
                    name = self._safe_action_name(a)
                    if any(tok in name for tok in ("build_settlement", "build_sett", "settle")):
                        base *= 1.5
                    if any(tok in name for tok in ("build_road", "road")):
                        base *= 1.3
                return base

            sorted_actions = sorted(actions, key=lambda a: score_for_rollout(a), reverse=True)

            # Try the top N actions until one simulates
            for a in sorted_actions[:6]:
                try:
                    branches = execute_deterministic(game, a)
                except Exception:
                    try:
                        branches = execute_spectrum(game, a)
                    except Exception:
                        branches = []
                if not branches:
                    continue
                # pick most probable branch
                best_branch = max(branches, key=lambda bp: float(bp[1]))
                next_game = best_branch[0]
                return self.rollout_value(next_game, color, depth - 1, initial=False)

            # fallback: try any action that simulates
            for a in sorted_actions[:10]:
                try:
                    branches = execute_deterministic(game, a)
                except Exception:
                    try:
                        branches = execute_spectrum(game, a)
                    except Exception:
                        branches = []
                if branches:
                    next_game = max(branches, key=lambda bp: float(bp[1]))[0]
                    return self.rollout_value(next_game, color, depth - 1, initial=False)

            return self._evaluate_game_state(game, color)
        except Exception as e:
            self.debug_print("FooPlayer: rollout_value exception, falling back to evaluate_game_state. Error:", e)
            return self._evaluate_game_state(game, color)

    # ------------------- Evaluate action expectation (enhanced) -------------------
    def _evaluate_action_expectation(self, game: Game, action: Any, per_action_branch_limit: int = 8) -> float:
        """Compute expected value of taking `action` in `game` for this player.

        Uses execute_spectrum when available then adds a rollout estimate for depth-1.
        """
        color = self._get_player_color()

        # Quick boosts for robber/knight/dev before heavy sim
        name = self._safe_action_name(action)
        preboost = 0.0
        try:
            if any(tok in name for tok in ("move_robber", "robber")):
                preboost += self.evaluate_robber_action(action, game, color)
            if any(tok in name for tok in ("knight", "play_knight")):
                preboost += self.evaluate_play_knight(action, game, color)
        except Exception:
            preboost += 0.0

        branches = None
        try:
            branches = execute_spectrum(game, action)
            if not branches:
                raise RuntimeError("execute_spectrum returned no branches")
        except Exception as e_s:
            self.debug_print("FooPlayer: execute_spectrum failed or unavailable for action; trying deterministic. Error:", e_s)
            try:
                branches = execute_deterministic(game, action)
                if not branches:
                    raise RuntimeError("execute_deterministic returned no outcomes")
            except Exception as e_d:
                self.debug_print("FooPlayer: Both execute_spectrum and execute_deterministic failed for action. Errors:", e_s, e_d)
                return float("-inf")

        # Limit branches to keep runtime bounded
        if len(branches) > per_action_branch_limit:
            branches = sorted(branches, key=lambda bp: float(bp[1]), reverse=True)[:per_action_branch_limit]

        expected = 0.0
        total_prob = 0.0
        rollout_depth = max(0, self.ROLLOUT_DEPTH - 1)
        for (out_game, prob) in branches:
            try:
                immediate = self._evaluate_game_state(out_game, color)
                rollout_est = self.rollout_value(out_game, color, rollout_depth, initial=True)
                branch_val = 0.6 * immediate + 0.4 * rollout_est
            except Exception as e:
                self.debug_print("FooPlayer: evaluation failed for branch, using heuristic. Error:", e)
                branch_val = self._heuristic_value(out_game, color)
            expected += float(prob) * float(branch_val)
            total_prob += float(prob)

        if total_prob > 0:
            expected = expected / total_prob

        expected += preboost
        return float(expected)

    # ------------------- Main decision function -------------------
    def decide(self, game: Game, playable_actions: Iterable) -> Optional[object]:
        """Choose an action from playable_actions using phase-aware sampling + rollouts."""
        try:
            playable_actions = list(playable_actions)
            if not playable_actions:
                self.debug_print("FooPlayer: No playable actions available, returning None")
                return None

            color = self._get_player_color()
            early = self.is_early_game(game)

            # Prefilter candidate actions
            candidates = self.prefilter_actions(playable_actions, game, color)

            # Cap to MAX_SIMULATIONS
            if len(candidates) > self.MAX_SIMULATIONS:
                candidates = candidates[: self.MAX_SIMULATIONS]

            if not candidates:
                candidates = random.sample(playable_actions, min(len(playable_actions), self.MAX_SIMULATIONS))

            # Distribute simulation budget adaptively
            per_action_budget = max(1, self.SIMULATION_BUDGET // max(1, len(candidates)))

            best_score = float("-inf")
            best_actions: List[Any] = []
            scores_debug: List[Tuple[float, Any]] = []

            for a in candidates:
                try:
                    score = self._evaluate_action_expectation(game, a, per_action_branch_limit=per_action_budget)
                except Exception as e:
                    self.debug_print("FooPlayer: Exception during action evaluation, skipping action. Error:", e)
                    score = float("-inf")

                scores_debug.append((score, a))

                if score > best_score:
                    best_score = score
                    best_actions = [a]
                elif score == best_score:
                    best_actions.append(a)

            # If no action had a finite score, fallback to first playable action
            if not best_actions:
                self.debug_print("FooPlayer: All evaluations failed, defaulting to first playable action")
                return playable_actions[0]

            # If tie, break ties preferring settlement/road/resource diversity improvements
            if len(best_actions) > 1:
                tie_metrics = []
                for a in best_actions:
                    try:
                        metric = 0.0
                        metric += self.settlement_potential(a, game, color)
                        metric += self.road_connection_potential(a, game, color)
                        # small production proxy via heuristic
                        metric += 0.01 * self._heuristic_value(game, color)
                        tie_metrics.append((metric, a))
                    except Exception:
                        tie_metrics.append((0.0, a))
                tie_metrics.sort(key=lambda x: x[0], reverse=True)
                # pick the top metric actions (could still be multiple)
                top_metric = tie_metrics[0][0]
                filtered = [a for (m, a) in tie_metrics if m == top_metric]
                if filtered:
                    chosen = random.choice(filtered)
                else:
                    chosen = random.choice(best_actions)
            else:
                chosen = best_actions[0]

            # Debug logging: phase and top candidates
            if self.DEBUG:
                phase = "EARLY" if early else "MID/LATE"
                self.debug_print(f"FooPlayer: Phase={phase}, chosen multipliers: settlement/road/city based on phase")
                scores_debug.sort(key=lambda x: x[0], reverse=True)
                topn = scores_debug[:3]
                self.debug_print("FooPlayer: Top candidates:")
                for sc, act in topn:
                    self.debug_print(f"  score={sc:.2f} action={act}")

            self.debug_print(f"FooPlayer: Chosen action {chosen} with expected score {best_score}")
            return chosen
        except Exception as e:
            # Protect against unexpected errors
            print("FooPlayer: Unexpected error in decide(), defaulting to first playable action. Error:", e)
            try:
                return list(playable_actions)[0]
            except Exception:
                return None

================================ Human Message =================================

This is the current adapters.py file that you must use to interact with the Catanatron API

"""Unified adapter for Catanatron agents.

Expose a small, stable surface for multi-agent systems to:
- Inspect game state
- Enumerate legal actions
- Execute hypothetical moves (with/without validation)
- Expand chance outcomes (dice, dev cards, robber)
- Use pruning helpers
- Build/evaluate heuristics

Everything here is a thin re-export or trivial wrapper from catanatron & friends.
"""

from typing import Callable, List, Optional, Tuple, Dict, Any

# CORE RE-EXPORTS
from catanatron.game import Game  # Game instance with .state, .copy(), .execute(action), .winning_color()
from catanatron.models.player import Player, Color  # Player and Color types
from catanatron.models.enums import Action, ActionType  # Action = namedtuple("Action", ["color", "action_type", "value"]) 

# Player and debug node classes (re-exported so consumers can import them from adapters)
from catanatron_experimental.machine_learning.players.minimax import (
    AlphaBetaPlayer,  # Player that executes an AlphaBeta search with expected value calculation
    SameTurnAlphaBetaPlayer,  # AlphaBeta constrained to the same turn
    DebugStateNode,  # Node for debugging the AlphaBeta search tree
    DebugActionNode,  # Node representing an action in the AlphaBeta search tree
)
from catanatron_experimental.machine_learning.players.value import (
    ValueFunctionPlayer,  # Player using heuristic value functions
    DEFAULT_WEIGHTS,  # Default weight set for value functions
)

# Underlying implementation imports (underscore aliases to avoid recursion)
from catanatron_experimental.machine_learning.players.tree_search_utils import (
    execute_deterministic as _execute_deterministic,
    execute_spectrum as _execute_spectrum,
    expand_spectrum as _expand_spectrum,
    list_prunned_actions as _list_prunned_actions,  # spelling verified in source
    prune_robber_actions as _prune_robber_actions,
)
from catanatron_experimental.machine_learning.players.minimax import render_debug_tree as _render_debug_tree

from catanatron_experimental.machine_learning.players.value import (
    base_fn as _base_fn,
    contender_fn as _contender_fn,
    value_production as _value_production,
    get_value_fn as _get_value_fn,
)

# Public API
__all__ = [
    "Game",
    "Player",
    "Color",
    "Action",
    "ActionType",
    "AlphaBetaPlayer",
    "SameTurnAlphaBetaPlayer",
    "ValueFunctionPlayer",
    "DebugStateNode",
    "DebugActionNode",
    "copy_game",
    "execute_deterministic",
    "execute_spectrum",
    "expand_spectrum",
    "list_prunned_actions",
    "prune_robber_actions",
    "render_debug_tree",
    "base_fn",
    "contender_fn",
    "value_production",
    "get_value_fn",
]

# THIN CONVENIENCE WRAPPERS
def copy_game(game: Game) -> Game:
    '''Create a deep copy of the game state.'''
    return game.copy()

def execute_deterministic(game: Game, action: Action) -> List[Tuple[Game, float]]:
    '''Execute a deterministic action and return the resulting game state with probability 1.'''
    return _execute_deterministic(game, action)

def execute_spectrum(game: Game, action: Action) -> List[Tuple[Game, float]]:
    '''Return a list of (game_copy, probability) tuples for all possible outcomes of an action.'''
    return _execute_spectrum(game, action)

def expand_spectrum(game: Game, actions: List[Action]) -> Dict[Action, List[Tuple[Game, float]]]:
    '''Expand a game state into all possible outcomes for a list of actions.'''
    return _expand_spectrum(game, actions)

def list_prunned_actions(game: Game) -> List[Action]:
    '''Returns a pruned list of actions to reduce the search space.'''
    return _list_prunned_actions(game)

def prune_robber_actions(current_color: Color, game: Game, actions: List[Action]) -> List[Action]:
    '''Prunes robber actions to keep only the most impactful ones.'''
    return _prune_robber_actions(current_color, game, actions)

def render_debug_tree(node: DebugStateNode) -> str:
    '''Renders the AlphaBeta search tree using Graphviz.'''
    return _render_debug_tree(node)

# HEURISTIC BUILDERS
def base_fn(params=DEFAULT_WEIGHTS) -> Callable[[Game, Color], float]:
    '''Base value function factory for evaluating game states.'''
    return _base_fn(params)

def contender_fn(params) -> Callable[[Game, Color], float]:
    '''Alternative value function factory with tuned weights.'''
    return _contender_fn(params)

def value_production(sample, player_name: str = "P0", include_variety: bool = True) -> float:
    '''Compute the production value of a player's state.'''
    return _value_production(sample, player_name, include_variety)

def get_value_fn(name: str, params, value_function=None) -> Callable[[Game, Color], float]:
    '''Factory that returns a value function by name and parameters.'''
    return _get_value_fn(name, params, value_function)


--- Output from CODER at 20250925_075842 ---
================================== Ai Message ==================================
Tool Calls:
  write_foo (call_SCONx0RI8Ya0h2PcEE2NX3b7)
 Call ID: call_SCONx0RI8Ya0h2PcEE2NX3b7
  Args:
    new_text: import random
from typing import Iterable, List, Optional, Any, Tuple

# MUST use the adapters surface to interact with the game environment
from .adapters import (
    Game,
    Player,
    Color,
    copy_game,
    execute_deterministic,
    execute_spectrum,
    expand_spectrum,
    list_prunned_actions,
    prune_robber_actions,
    base_fn,
    value_production,
    get_value_fn,
)


class FooPlayer(Player):
    """A Foo player with game-phase aware decisioning, improved sampling,
    short rollouts, and richer heuristics.

    This implementation is defensive: it uses only the adapters surface and
    contains many fallbacks when attributes or adapter helpers are missing.

    Key features:
    - Game-phase detection (early/mid/late) to bias settlement/road vs city/dev-card
    - Settlement & road potential heuristics to encourage early expansion
    - Robber/knight evaluation to value disruption and steals
    - Must-include guarantees for critical action types (settlement/road/robber/dev)
    - Rollout policy biased by phase and includes a light opponent-response

    NOTE: Many game model attribute names vary across environments. This code
    attempts multiple common attribute names and falls back to string-based
    heuristics when necessary. If the next run raises AttributeError for an
    adapters function or a specific attribute, provide the traceback so it can
    be patched to the concrete environment.
    """

    # Tunable constants (exposed to edit for experimentation)
    MAX_SIMULATIONS = 24
    PREFILTER_TOP_K = 8
    ROLLOUT_DEPTH = 2
    SIMULATION_BUDGET = 60
    DEBUG = False

    # Phase thresholds
    EARLY_TURN_THRESHOLD = 25
    EARLY_VP_THRESHOLD = 5

    # Stronger early-game multipliers (updated per latest tuning)
    EARLY_SETTLEMENT_MULT = 2.5
    EARLY_ROAD_MULT = 2.0
    EARLY_CITY_MULT = 0.7
    EARLY_DEV_MULT = 1.25

    # Mid/Late multipliers
    MID_SETTLEMENT_MULT = 1.0
    MID_ROAD_MULT = 1.0
    MID_CITY_MULT = 1.25
    MID_DEV_MULT = 1.0

    # Must-include action tokens (robust, lowercase matching)
    MUST_INCLUDE_TOKENS = {
        "build_city",
        "build_settlement",
        "build_sett",
        "build_road",
        "buy_dev",
        "buy_dev_card",
        "buycard",
        "play_knight",
        "knight",
        "move_robber",
        "move_robber_action",
        "robber",
        "trade",
        "offer_trade",
    }

    # Robber scoring base (increased)
    ROBBER_BASE_SCORE = 60.0

    # Settlement target in early game
    TARGET_SETTLEMENTS_EARLY = 3

    # Epsilon-greedy randomness to avoid predictability
    EPSILON_GREEDY = 0.08

    # Rollout bonuses for the very first rollout step
    ROLLOUT_SETTLEMENT_BONUS = 1.7
    ROLLOUT_ROAD_BONUS = 1.4

    # Tie tolerance
    TOLERANCE = 1e-6

    def __init__(self, name: Optional[str] = None):
        super().__init__(Color.BLUE, name)
        # Try to cache a base value function from adapters
        try:
            self._value_fn = base_fn()
            self.debug_print("FooPlayer: Using adapters.base_fn() for evaluation")
        except Exception as e:
            self._value_fn = None
            self.debug_print("FooPlayer: adapters.base_fn() not available, will use heuristic. Error:", e)

    # ------------------- Debug helper -------------------
    def debug_print(self, *args: Any) -> None:
        if self.DEBUG:
            print(*args)

    # ------------------- Utility helpers -------------------
    def _get_player_color(self) -> Color:
        """Return this player's color. Try common attribute names."""
        if hasattr(self, "color"):
            return getattr(self, "color")
        if hasattr(self, "_color"):
            return getattr(self, "_color")
        return Color.BLUE

    def _safe_action_name(self, action: Any) -> str:
        """Produce a lowercase string name for the action for robust matching."""
        try:
            at = getattr(action, "action_type", None)
            if at is None:
                at = getattr(action, "type", None)
            if at is not None:
                try:
                    return str(at.name).lower()
                except Exception:
                    return str(at).lower()
        except Exception:
            pass
        try:
            # Some Action objects have a .name or .action_name
            name = getattr(action, "name", None) or getattr(action, "action_name", None)
            if name is not None:
                return str(name).lower()
        except Exception:
            pass
        try:
            return str(action).lower()
        except Exception:
            return ""

    # ------------------- Phase detection -------------------
    def is_early_game(self, game: Game) -> bool:
        """Heuristic to detect early phase of the game.

        Prefer explicit turn counts if available, else fallback to max VP.
        """
        # Try turn/tick fields
        try:
            state = getattr(game, "state", game)
            turn_count = (
                getattr(state, "turn", None)
                or getattr(state, "tick", None)
                or getattr(state, "turn_count", None)
                or getattr(state, "tick_count", None)
            )
            if isinstance(turn_count, (int, float)):
                return int(turn_count) < self.EARLY_TURN_THRESHOLD
        except Exception:
            pass

        # Fallback: use maximum VP among players
        try:
            players = getattr(state, "players", None) or getattr(game, "players", None) or []
            max_vp = 0
            if isinstance(players, dict):
                for p in players.values():
                    vp = getattr(p, "victory_points", None) or getattr(p, "vp", None) or 0
                    try:
                        vp = int(vp)
                    except Exception:
                        vp = 0
                    if vp > max_vp:
                        max_vp = vp
            else:
                for p in players:
                    vp = getattr(p, "victory_points", None) or getattr(p, "vp", None) or 0
                    try:
                        vp = int(vp)
                    except Exception:
                        vp = 0
                    if vp > max_vp:
                        max_vp = vp
            return max_vp < self.EARLY_VP_THRESHOLD
        except Exception:
            # Conservative fallback: treat as mid-game
            return False

    # ------------------- Heuristic / evaluation (phase-aware) -------------------
    def _heuristic_value(self, game: Game, color: Color) -> float:
        """Phase-aware heuristic including production potential and city-upgrade progress.

        Many attribute names are attempted to be robust across different game models.
        """
        # Die probabilities for numbers 2..12 ignoring 7
        die_prob = {2: 1 / 36, 3: 2 / 36, 4: 3 / 36, 5: 4 / 36, 6: 5 / 36, 8: 5 / 36, 9: 4 / 36, 10: 3 / 36, 11: 2 / 36, 12: 1 / 36}

        # Player lookup
        player_state = None
        try:
            state = getattr(game, "state", game)
            players = getattr(state, "players", None) or getattr(game, "players", None)
            if isinstance(players, dict):
                player_state = players.get(color) or players.get(str(color))
            elif isinstance(players, (list, tuple)):
                for p in players:
                    if getattr(p, "color", None) == color or getattr(p, "color", None) == str(color):
                        player_state = p
                        break
        except Exception:
            player_state = None

        def _safe_get(obj, *names, default=0):
            if obj is None:
                return default
            for name in names:
                try:
                    val = getattr(obj, name)
                    if val is not None:
                        return val
                except Exception:
                    try:
                        val = obj[name]
                        if val is not None:
                            return val
                    except Exception:
                        continue
            return default

        vp = _safe_get(player_state, "victory_points", "vp", default=0)
        settlements = _safe_get(player_state, "settlements", "settle_count", "settle_locations", default=0)
        if isinstance(settlements, (list, tuple)):
            settlements = len(settlements)
        cities = _safe_get(player_state, "cities", "city_count", "city_locations", default=0)
        if isinstance(cities, (list, tuple)):
            cities = len(cities)
        roads = _safe_get(player_state, "roads", "road_count", default=0)
        if isinstance(roads, (list, tuple)):
            roads = len(roads)
        dev_vp = _safe_get(player_state, "dev_vp", "dev_victory_points", default=0)

        # Resources summary
        resources_obj = _safe_get(player_state, "resources", default=0)
        resources_total = 0
        resource_diversity = 0
        try:
            if isinstance(resources_obj, dict):
                resources_total = sum(resources_obj.values())
                resource_diversity = sum(1 for v in resources_obj.values() if v > 0)
            elif isinstance(resources_obj, (list, tuple)):
                resources_total = sum(resources_obj)
                resource_diversity = sum(1 for v in resources_obj if v > 0)
            else:
                resources_total = int(resources_obj)
                resource_diversity = 1 if resources_total > 0 else 0
        except Exception:
            resources_total = 0
            resource_diversity = 0

        # Production potential estimation
        prod_value = 0.0
        try:
            board = getattr(state, "board", None) or getattr(game, "board", None)
            hexes = getattr(board, "hexes", None) or getattr(board, "tiles", None) or []
            settlements_list = _safe_get(player_state, "settlements", "settle_locations", default=[])
            if isinstance(settlements_list, (list, tuple)):
                for s in settlements_list:
                    try:
                        for h in hexes:
                            neighbors = getattr(h, "vertices", None) or getattr(h, "adjacent_vertices", None) or []
                            if s in neighbors:
                                num = getattr(h, "roll", None) or getattr(h, "number", None) or getattr(h, "value", None)
                                try:
                                    num = int(num)
                                except Exception:
                                    num = None
                                if num in die_prob:
                                    prod_value += die_prob[num] * 1.0
                    except Exception:
                        continue
            cities_list = _safe_get(player_state, "cities", "city_locations", default=[])
            if isinstance(cities_list, (list, tuple)):
                for c in cities_list:
                    try:
                        for h in hexes:
                            neighbors = getattr(h, "vertices", None) or getattr(h, "adjacent_vertices", None) or []
                            if c in neighbors:
                                num = getattr(h, "roll", None) or getattr(h, "number", None) or getattr(h, "value", None)
                                try:
                                    num = int(num)
                                except Exception:
                                    num = None
                                if num in die_prob:
                                    prod_value += die_prob[num] * 2.0
                    except Exception:
                        continue
        except Exception:
            prod_value = 0.0

        # City upgrade progress heuristic
        city_resource_val = 0.0
        try:
            if isinstance(resources_obj, dict):
                wheat = resources_obj.get("wheat", 0) + resources_obj.get("grain", 0)
                ore = resources_obj.get("ore", 0) + resources_obj.get("metal", 0)
                city_resource_val = min(wheat, ore)
        except Exception:
            city_resource_val = 0.0

        # Phase multipliers
        early = self.is_early_game(game)
        if early:
            settlement_mul = self.EARLY_SETTLEMENT_MULT
            road_mul = self.EARLY_ROAD_MULT
            city_mul = self.EARLY_CITY_MULT
            dev_mul = self.EARLY_DEV_MULT
            prod_weight = 90.0
        else:
            settlement_mul = self.MID_SETTLEMENT_MULT
            road_mul = self.MID_ROAD_MULT
            city_mul = self.MID_CITY_MULT
            dev_mul = self.MID_DEV_MULT
            prod_weight = 40.0

        # Compose weighted sum (city reward slightly reduced in early game by city_mul)
        score = (
            float(vp) * 100.0
            + float(settlements) * 25.0 * settlement_mul
            + float(cities) * 60.0 * city_mul
            + float(roads) * 6.0 * road_mul
            + float(dev_vp) * 50.0
            + float(resources_total) * 1.0
            + float(resource_diversity) * 3.0
            + float(city_resource_val) * 5.0
            + float(prod_value) * prod_weight
        )

        return float(score)

    def _evaluate_game_state(self, game: Game, color: Color) -> float:
        """Evaluate a single game state for the given player color.

        Prefer adapters.base_fn() if available (cached in self._value_fn). If available, combine
        it with the heuristic for stability. We keep phase multipliers inside the heuristic so
        they influence the final blended value.
        """
        heuristic = self._heuristic_value(game, color)
        if self._value_fn is not None:
            try:
                vf_val = float(self._value_fn(game, color))
                return 0.85 * vf_val + 0.15 * heuristic
            except Exception as e:
                self.debug_print("FooPlayer: value_fn failed during evaluate_game_state, falling back to heuristic. Error:", e)
        return float(heuristic)

    # ------------------- Cheap scoring & potentials -------------------
    def _get_player_state(self, game: Game, color: Color) -> Any:
        """Return the player_state object from the game state (best-effort)."""
        try:
            state = getattr(game, "state", game)
            players = getattr(state, "players", None) or getattr(game, "players", None)
            if isinstance(players, dict):
                return players.get(color) or players.get(str(color))
            elif isinstance(players, (list, tuple)):
                for p in players:
                    if getattr(p, "color", None) == color or getattr(p, "color", None) == str(color):
                        return p
        except Exception:
            return None
        return None

    def settlement_potential(self, action: Any, game: Game, color: Color) -> float:
        """Estimate benefit of a settlement action: new resource types and production.

        Best-effort: try to parse adjacent hexes from action or fallback to string heuristics.
        """
        bonus = 0.0
        try:
            name = self._safe_action_name(action)
            # Quick check: if action indicates a settlement, give base
            if any(tok in name for tok in ("build_settlement", "build_sett", "settle")):
                bonus += 5.0

            # Try to parse a vertex index from the action string
            digits = [int(tok) for tok in name.split() if tok.isdigit()]
            vertex = digits[0] if digits else None

            state = getattr(game, "state", game)
            board = getattr(state, "board", None) or getattr(game, "board", None)
            hexes = getattr(board, "hexes", None) or getattr(board, "tiles", None) or []

            # Player's current resource types
            player_state = self._get_player_state(game, color)
            player_types = set()
            try:
                settlements_list = getattr(player_state, "settlements", None) or getattr(player_state, "settle_locations", None) or []
                if isinstance(settlements_list, (list, tuple)):
                    for s in settlements_list:
                        for h in hexes:
                            neighbors = getattr(h, "vertices", None) or getattr(h, "adjacent_vertices", None) or []
                            if s in neighbors:
                                rtype = getattr(h, "resource", None) or getattr(h, "type", None)
                                if rtype is not None:
                                    player_types.add(str(rtype).lower())
            except Exception:
                player_types = set()

            # Adjacent resources for proposed vertex
            adj_resources = set()
            prod_sum = 0.0
            die_prob = {2: 1 / 36, 3: 2 / 36, 4: 3 / 36, 5: 4 / 36, 6: 5 / 36, 8: 5 / 36, 9: 4 / 36, 10: 3 / 36, 11: 2 / 36, 12: 1 / 36}
            if vertex is not None:
                for h in hexes:
                    try:
                        neighbors = getattr(h, "vertices", None) or getattr(h, "adjacent_vertices", None) or []
                        if vertex in neighbors:
                            rtype = getattr(h, "resource", None) or getattr(h, "type", None)
                            if rtype is not None:
                                adj_resources.add(str(rtype).lower())
                            num = getattr(h, "roll", None) or getattr(h, "number", None) or getattr(h, "value", None)
                            try:
                                num = int(num)
                            except Exception:
                                num = None
                            if num in die_prob:
                                prod_sum += die_prob[num]
                    except Exception:
                        continue
            # New types
            new_types = adj_resources - player_types
            bonus += float(len(new_types)) * 12.0
            bonus += float(prod_sum) * 8.0
        except Exception:
            pass
        return float(bonus)

    def road_connection_potential(self, action: Any, game: Game, color: Color) -> float:
        """Estimate if a road action helps expansion. Best-effort using indices."""
        bonus = 0.0
        try:
            name = self._safe_action_name(action)
            # try to extract numbers from action name
            digits = [int(tok) for tok in name.split() if tok.isdigit()]
            # player's settlement/city vertices
            player_state = self._get_player_state(game, color)
            player_nodes = set()
            try:
                settles = getattr(player_state, "settlements", None) or getattr(player_state, "settle_locations", None) or []
                cities = getattr(player_state, "cities", None) or getattr(player_state, "city_locations", None) or []
                if isinstance(settles, (list, tuple)):
                    player_nodes.update(settles)
                if isinstance(cities, (list, tuple)):
                    player_nodes.update(cities)
            except Exception:
                player_nodes = set()

            if digits:
                # if any digit matches a player node, give higher bonus
                if any(d in player_nodes for d in digits):
                    bonus += 6.0
                else:
                    bonus += 3.0
            else:
                # fallback string heuristics
                if "build_road" in name or ("road" in name and "build" in name):
                    bonus += 2.0
        except Exception:
            pass
        return float(bonus)

    def evaluate_buy_dev_card(self, action: Any, game: Game, color: Color) -> bool:
        """Decide whether buying a dev card is currently a good idea (best-effort)."""
        try:
            player_state = self._get_player_state(game, color)
            resources = getattr(player_state, "resources", None)
            if isinstance(resources, dict):
                ore = resources.get("ore", 0) + resources.get("metal", 0)
                wheat = resources.get("wheat", 0) + resources.get("grain", 0)
                others = sum(v for k, v in resources.items() if k not in ("ore", "metal", "wheat", "grain"))
                # if have ore+wheat+another, prefer dev card; or if no settlement/road/city affordable
                if ore >= 1 and wheat >= 1 and others >= 1:
                    return True
                # fallback: if early game and we have some resources but no settlement potential, allow dev buy
                if self.is_early_game(game) and (ore + wheat + others) >= 3:
                    return True
        except Exception:
            pass
        return False

    def cheap_pre_score(self, action: Any, game: Game, color: Color) -> float:
        """Cheap, fast scoring used to prioritize actions for simulation (phase-aware)."""
        s = 0.0
        name = self._safe_action_name(action)

        early = self.is_early_game(game)
        # multipliers
        if early:
            settlement_mul = self.EARLY_SETTLEMENT_MULT
            road_mul = self.EARLY_ROAD_MULT
            city_mul = self.EARLY_CITY_MULT
            dev_mul = self.EARLY_DEV_MULT
        else:
            settlement_mul = self.MID_SETTLEMENT_MULT
            road_mul = self.MID_ROAD_MULT
            city_mul = self.MID_CITY_MULT
            dev_mul = self.MID_DEV_MULT

        # Reward direct VP gains but reduce city bias early
        if any(tok in name for tok in ("build_city",)):
            s += max(0.0, 100.0 * city_mul - 15.0)  # slightly reduced base for cities
        if any(tok in name for tok in ("build_settlement", "build_sett")):
            s += 90.0 * settlement_mul
            # add settlement potential (resource diversity / production)
            s += self.settlement_potential(action, game, color) * (1.0 if not early else settlement_mul)
        if "buy_dev" in name or "buycard" in name or "buy_dev_card" in name:
            buy_pref = self.evaluate_buy_dev_card(action, game, color)
            if buy_pref:
                s += 30.0 * dev_mul + 30.0  # boosted when recommended
            else:
                s += 20.0 * dev_mul
        if "build_road" in name or ("road" in name and "build" in name):
            s += 20.0 * road_mul
            s += self.road_connection_potential(action, game, color) * (1.0 if not early else road_mul)
        if "knight" in name or "play_knight" in name:
            s += 70.0
            s += self.evaluate_play_knight(action, game, color)
        if "robber" in name or "move_robber" in name:
            s += 50.0
            s += self.evaluate_robber_action(action, game, color)
        if "trade" in name or "offer_trade" in name:
            s += 10.0

        # Encourage hitting settlement target early
        try:
            player_state = self._get_player_state(game, color)
            curr_settlements = 0
            settles = getattr(player_state, "settlements", None) or getattr(player_state, "settle_locations", None) or []
            if isinstance(settles, (list, tuple)):
                curr_settlements = len(settles)
            if early and curr_settlements < self.TARGET_SETTLEMENTS_EARLY and any(tok in name for tok in ("build_settlement", "build_sett")):
                s += 30.0
        except Exception:
            pass

        # Add small potentials for roads/settlements if not already included
        if not any(tok in name for tok in ("build_settlement", "build_sett")):
            s += self.settlement_potential(action, game, color) * 0.1
        if not any(tok in name for tok in ("build_road",)):
            s += self.road_connection_potential(action, game, color) * 0.1

        # Minor random tie-break
        s += random.random() * 1e-3
        return s

    # ------------------- Prefilter actions (phase-aware guarantees) -------------------
    def prefilter_actions(self, actions: List[Any], game: Game, color: Color) -> List[Any]:
        """Return a bounded list of candidate actions to evaluate thoroughly.

        Guarantees inclusion of must-include tokens and early-game settlement/road actions.
        """
        if not actions:
            return []

        all_actions = list(actions)
        early = self.is_early_game(game)

        musts = []
        others = []
        found_settlement = None
        found_road = None
        for a in all_actions:
            name = self._safe_action_name(a)
            if any(tok in name for tok in self.MUST_INCLUDE_TOKENS):
                if a not in musts:
                    musts.append(a)
            else:
                others.append(a)
            if found_settlement is None and any(tok in name for tok in ("build_settlement", "build_sett", "settle")):
                found_settlement = a
            if found_road is None and any(tok in name for tok in ("build_road", "road")):
                found_road = a

        # Early-game forced includes: ensure at least one settlement and one road action if present
        if early:
            if found_settlement is not None and found_settlement not in musts:
                musts.append(found_settlement)
            if found_road is not None and found_road not in musts:
                musts.append(found_road)

        # Also include recommended dev-card buys
        for a in all_actions:
            name = self._safe_action_name(a)
            if any(tok in name for tok in ("buy_dev", "buycard", "buy_dev_card")):
                try:
                    if self.evaluate_buy_dev_card(a, game, color) and a not in musts:
                        musts.append(a)
                except Exception:
                    pass

        # Ensure robber/knight actions are present
        for a in all_actions:
            name = self._safe_action_name(a)
            if any(tok in name for tok in ("robber", "move_robber", "knight", "play_knight")):
                if a not in musts:
                    musts.append(a)

        # Score and pick top-K from others
        scored = [(self.cheap_pre_score(a, game, color), a) for a in others]
        scored.sort(key=lambda x: x[0], reverse=True)
        top_k = [a for (_s, a) in scored[: self.PREFILTER_TOP_K]]

        # Combine unique musts + top_k preserving order
        candidates = []
        for a in musts + top_k:
            if a not in candidates:
                candidates.append(a)

        # Fill up with random remaining samples until MAX_SIMULATIONS
        remaining = [a for a in all_actions if a not in candidates]
        random.shuffle(remaining)
        while len(candidates) < min(len(all_actions), self.MAX_SIMULATIONS) and remaining:
            candidates.append(remaining.pop())

        if not candidates and all_actions:
            candidates = random.sample(all_actions, min(len(all_actions), self.MAX_SIMULATIONS))

        self.debug_print(f"FooPlayer: Prefilter selected {len(candidates)} candidates (musts={len(musts)}, early={early})")
        if self.DEBUG and early:
            self.debug_print(f"  Forced includes: settlement={'yes' if found_settlement else 'no'}, road={'yes' if found_road else 'no'}")
        return candidates

    # ------------------- Playable actions extraction -------------------
    def get_playable_actions_from_game(self, game: Game) -> List[Any]:
        """Try adapters.list_prunned_actions first, then common game attributes."""
        try:
            acts = list_prunned_actions(game)
            if acts:
                return acts
        except Exception as e:
            self.debug_print("FooPlayer: list_prunned_actions unavailable or failed. Error:", e)

        try:
            if hasattr(game, "get_playable_actions"):
                return list(game.get_playable_actions())
        except Exception:
            pass
        try:
            if hasattr(game, "playable_actions"):
                return list(getattr(game, "playable_actions"))
        except Exception:
            pass
        try:
            state = getattr(game, "state", None)
            if state is not None and hasattr(state, "playable_actions"):
                return list(getattr(state, "playable_actions"))
        except Exception:
            pass

        return []

    # ------------------- Robber / Knight evaluation -------------------
    def evaluate_robber_action(self, action: Any, game: Game, color: Color) -> float:
        """Estimate the value of moving the robber (best-effort)."""
        score = 0.0
        try:
            # Base preference to include robber moves
            score += self.ROBBER_BASE_SCORE
            name = self._safe_action_name(action)
            # Try to parse a target hex id
            digits = [int(tok) for tok in name.split() if tok.isdigit()]
            target = digits[0] if digits else None

            # Die probabilities
            die_prob = {2: 1 / 36, 3: 2 / 36, 4: 3 / 36, 5: 4 / 36, 6: 5 / 36, 8: 5 / 36, 9: 4 / 36, 10: 3 / 36, 11: 2 / 36, 12: 1 / 36}

            state = getattr(game, "state", game)
            board = getattr(state, "board", None) or getattr(game, "board", None)
            hexes = getattr(board, "hexes", None) or getattr(board, "tiles", None) or []

            # Map hex identifier to object (best-effort: use index or id)
            hex_map = {}
            for idx, h in enumerate(hexes):
                try:
                    hid = getattr(h, "id", None) or getattr(h, "index", None) or idx
                except Exception:
                    hid = idx
                try:
                    key = int(hid) if isinstance(hid, int) or (isinstance(hid, str) and hid.isdigit()) else idx
                except Exception:
                    key = idx
                hex_map[key] = h

            # Compute production loss on opponents
            opponents = []
            players = getattr(state, "players", None) or getattr(game, "players", None) or []
            if isinstance(players, dict):
                for k, p in players.items():
                    if k == color or getattr(p, "color", None) == color:
                        continue
                    opponents.append(p)
            else:
                for p in players:
                    if getattr(p, "color", None) == color or p == color:
                        continue
                    opponents.append(p)

            total_prod_loss = 0.0
            steal_expected = 0.0
            resource_value = {"ore": 3.0, "metal": 3.0, "wheat": 3.0, "grain": 3.0, "brick": 2.0, "lumber": 2.0, "wood": 2.0, "sheep": 2.0}

            if target in hex_map:
                h = hex_map[target]
                num = getattr(h, "roll", None) or getattr(h, "number", None) or getattr(h, "value", None)
                try:
                    num = int(num)
                except Exception:
                    num = None
                prob = die_prob.get(num, 0)
                # For each opponent, check their settlements/cities adjacent to this hex
                for opp in opponents:
                    opp_settles = getattr(opp, "settlements", None) or getattr(opp, "settle_locations", None) or []
                    opp_cities = getattr(opp, "cities", None) or getattr(opp, "city_locations", None) or []
                    mult = 0.0
                    try:
                        for s in opp_settles:
                            neighbors = getattr(h, "vertices", None) or getattr(h, "adjacent_vertices", None) or []
                            if s in neighbors:
                                mult += 1.0
                        for c in opp_cities:
                            neighbors = getattr(h, "vertices", None) or getattr(h, "adjacent_vertices", None) or []
                            if c in neighbors:
                                mult += 2.0
                    except Exception:
                        continue
                    total_prod_loss += prob * mult
                    # Estimate steal expected value as portion of opponent resources times average resource value
                    try:
                        opp_resources = getattr(opp, "resources", None) or {}
                        if isinstance(opp_resources, dict) and opp_resources:
                            total_res = sum(opp_resources.values())
                            if total_res > 0:
                                # expected stolen resource value ~ average resource weight * 1/total_res
                                avg_val = sum(resource_value.get(r, 1.5) * (opp_resources.get(r, 0) / total_res) for r in opp_resources)
                                steal_expected += avg_val * 0.5
                    except Exception:
                        pass

            # Scale components (more aggressive)
            score += total_prod_loss * 40.0
            score += steal_expected * 12.0
            # Extra bonus if multiple opponent cities affected
            try:
                if target in hex_map:
                    h = hex_map[target]
                    city_count = 0
                    for opp in opponents:
                        for c in getattr(opp, "cities", []) or getattr(opp, "city_locations", []) or []:
                            neighbors = getattr(h, "vertices", None) or getattr(h, "adjacent_vertices", None) or []
                            if c in neighbors:
                                city_count += 1
                    if city_count > 1:
                        score += 20.0 * city_count
            except Exception:
                pass

        except Exception:
            pass
        return float(score)

    def evaluate_play_knight(self, action: Any, game: Game, color: Color) -> float:
        """Estimate the value of playing a knight (best-effort)."""
        score = 10.0
        try:
            # base
            score += 5.0
            # detect steal / robber keywords
            name = self._safe_action_name(action)
            if "steal" in name or "rob" in name:
                score += 10.0

            # army progress
            player_state = self._get_player_state(game, color)
            army = getattr(player_state, "army", None) or getattr(player_state, "army_size", None) or getattr(player_state, "knights_played", None) or 0
            try:
                army = int(army)
            except Exception:
                army = 0
            # if close to largest army threshold (2 or 3), add bonus
            if army >= 2:
                score += 30.0
            else:
                score += 15.0
        except Exception:
            pass
        return float(score)

    # ------------------- Helper: determine active player color -------------------
    def _get_active_player_color(self, game: Game) -> Optional[Color]:
        """Best-effort to detect which Color is to move in the given game state."""
        try:
            state = getattr(game, "state", game)
            cp = getattr(state, "current_player", None) or getattr(state, "active_player", None) or getattr(state, "turn_color", None)
            if cp is None:
                cp = getattr(game, "current_player", None)
            # cp might be index, player object, or Color
            if isinstance(cp, Color):
                return cp
            if isinstance(cp, int):
                players = getattr(state, "players", None) or getattr(game, "players", None) or []
                try:
                    if isinstance(players, (list, tuple)) and 0 <= cp < len(players):
                        return getattr(players[cp], "color", None)
                except Exception:
                    pass
            # If cp is a player object
            if hasattr(cp, "color"):
                return getattr(cp, "color")

            # Fallback: pick first player in players whose color != our color
            players = getattr(state, "players", None) or getattr(game, "players", None) or []
            my_color = self._get_player_color()
            if isinstance(players, dict):
                for k, p in players.items():
                    try:
                        c = getattr(p, "color", None) or k
                        if c != my_color:
                            return c
                    except Exception:
                        continue
            else:
                for p in players:
                    try:
                        c = getattr(p, "color", None)
                        if c != my_color:
                            return c
                    except Exception:
                        continue
        except Exception:
            pass
        return None

    # ------------------- Rollout logic with opponent-response -------------------
    def rollout_value(self, game: Game, color: Color, depth: int, initial: bool = True) -> float:
        """Short greedy rollout with phase bias and light opponent-response.

        initial: True for the first step of rollout so we can bias toward expansion early.
        """
        try:
            if depth <= 0:
                return self._evaluate_game_state(game, color)

            actions = self.get_playable_actions_from_game(game)
            if not actions:
                return self._evaluate_game_state(game, color)

            early = self.is_early_game(game)

            def score_for_rollout(a, g, c, is_initial):
                base = self.cheap_pre_score(a, g, c)
                if is_initial and early:
                    name = self._safe_action_name(a)
                    if any(tok in name for tok in ("build_settlement", "build_sett", "settle")):
                        base *= self.ROLLOUT_SETTLEMENT_BONUS
                    if any(tok in name for tok in ("build_road", "road")):
                        base *= self.ROLLOUT_ROAD_BONUS
                return base

            sorted_actions = sorted(actions, key=lambda a: score_for_rollout(a, game, color, initial), reverse=True)

            # Try top actions to simulate
            for a in sorted_actions[:6]:
                branches = []
                try:
                    branches = execute_deterministic(game, a)
                except Exception:
                    try:
                        branches = execute_spectrum(game, a)
                    except Exception:
                        branches = []
                if not branches:
                    continue
                # pick the most probable branch
                next_game = max(branches, key=lambda bp: float(bp[1]))[0]

                # Light opponent-response: if opponent to move next, simulate their greedy action once
                opp_color = self._get_active_player_color(next_game)
                my_color = color
                if opp_color is not None and opp_color != my_color:
                    try:
                        opp_actions = self.get_playable_actions_from_game(next_game)
                        if opp_actions:
                            # pick opponent best action by cheap_pre_score from their perspective
                            best_opp = max(opp_actions, key=lambda oa: self.cheap_pre_score(oa, next_game, opp_color))
                            # simulate opponent action deterministically if possible
                            opp_branches = []
                            try:
                                opp_branches = execute_deterministic(next_game, best_opp)
                            except Exception:
                                try:
                                    opp_branches = execute_spectrum(next_game, best_opp)
                                except Exception:
                                    opp_branches = []
                            if opp_branches:
                                next_game = max(opp_branches, key=lambda bp: float(bp[1]))[0]
                    except Exception:
                        pass

                return self.rollout_value(next_game, color, depth - 1, initial=False)

            # fallback: try any action that simulates
            for a in sorted_actions[:10]:
                branches = []
                try:
                    branches = execute_deterministic(game, a)
                except Exception:
                    try:
                        branches = execute_spectrum(game, a)
                    except Exception:
                        branches = []
                if branches:
                    next_game = max(branches, key=lambda bp: float(bp[1]))[0]
                    return self.rollout_value(next_game, color, depth - 1, initial=False)

            return self._evaluate_game_state(game, color)
        except Exception as e:
            self.debug_print("FooPlayer: rollout_value exception, falling back to evaluate_game_state. Error:", e)
            return self._evaluate_game_state(game, color)

    # ------------------- Evaluate action expectation (enhanced) -------------------
    def _evaluate_action_expectation(self, game: Game, action: Any, per_action_branch_limit: int = 8) -> float:
        """Compute expected value of taking `action` in `game` for this player.

        Uses execute_spectrum when available then adds a rollout estimate for depth-1.
        """
        color = self._get_player_color()

        # Quick boosts for robber/knight/dev before heavy sim
        name = self._safe_action_name(action)
        preboost = 0.0
        try:
            if any(tok in name for tok in ("move_robber", "robber")):
                preboost += self.evaluate_robber_action(action, game, color)
            if any(tok in name for tok in ("knight", "play_knight")):
                preboost += self.evaluate_play_knight(action, game, color)
            if any(tok in name for tok in ("buy_dev", "buycard", "buy_dev_card")):
                if self.evaluate_buy_dev_card(action, game, color):
                    preboost += 20.0
        except Exception:
            preboost += 0.0

        branches = None
        try:
            branches = execute_spectrum(game, action)
            if not branches:
                raise RuntimeError("execute_spectrum returned no branches")
        except Exception as e_s:
            self.debug_print("FooPlayer: execute_spectrum failed or unavailable for action; trying deterministic. Error:", e_s)
            try:
                branches = execute_deterministic(game, action)
                if not branches:
                    raise RuntimeError("execute_deterministic returned no outcomes")
            except Exception as e_d:
                self.debug_print("FooPlayer: Both execute_spectrum and execute_deterministic failed for action. Errors:", e_s, e_d)
                return float("-inf")

        # Limit branches to keep runtime bounded
        if len(branches) > per_action_branch_limit:
            branches = sorted(branches, key=lambda bp: float(bp[1]), reverse=True)[:per_action_branch_limit]

        expected = 0.0
        total_prob = 0.0
        rollout_depth = max(0, self.ROLLOUT_DEPTH - 1)
        for (out_game, prob) in branches:
            try:
                immediate = self._evaluate_game_state(out_game, color)
                rollout_est = self.rollout_value(out_game, color, rollout_depth, initial=True)
                branch_val = 0.6 * immediate + 0.4 * rollout_est
            except Exception as e:
                self.debug_print("FooPlayer: evaluation failed for branch, using heuristic. Error:", e)
                branch_val = self._heuristic_value(out_game, color)
            expected += float(prob) * float(branch_val)
            total_prob += float(prob)

        if total_prob > 0:
            expected = expected / total_prob

        expected += preboost
        return float(expected)

    # ------------------- Main decision function -------------------
    def decide(self, game: Game, playable_actions: Iterable) -> Optional[object]:
        """Choose an action from playable_actions using phase-aware sampling + rollouts."""
        try:
            playable_actions = list(playable_actions)
            if not playable_actions:
                self.debug_print("FooPlayer: No playable actions available, returning None")
                return None

            color = self._get_player_color()
            early = self.is_early_game(game)

            # Prefilter candidate actions
            candidates = self.prefilter_actions(playable_actions, game, color)

            # Cap to MAX_SIMULATIONS
            if len(candidates) > self.MAX_SIMULATIONS:
                candidates = candidates[: self.MAX_SIMULATIONS]

            if not candidates:
                candidates = random.sample(playable_actions, min(len(playable_actions), self.MAX_SIMULATIONS))

            # Distribute simulation budget adaptively
            per_action_budget = max(1, self.SIMULATION_BUDGET // max(1, len(candidates)))

            best_score = float("-inf")
            best_actions: List[Any] = []
            scores_debug: List[Tuple[float, Any]] = []

            for a in candidates:
                try:
                    score = self._evaluate_action_expectation(game, a, per_action_branch_limit=per_action_budget)
                except Exception as e:
                    self.debug_print("FooPlayer: Exception during action evaluation, skipping action. Error:", e)
                    score = float("-inf")

                scores_debug.append((score, a))

                if score > best_score + self.TOLERANCE:
                    best_score = score
                    best_actions = [a]
                elif abs(score - best_score) <= self.TOLERANCE:
                    best_actions.append(a)

            # If no action had a finite score, fallback to first playable action
            if not best_actions:
                self.debug_print("FooPlayer: All evaluations failed, defaulting to first playable action")
                return playable_actions[0]

            # Epsilon-greedy randomness to reduce predictability
            chosen: Any
            scores_debug.sort(key=lambda x: x[0], reverse=True)
            if random.random() < self.EPSILON_GREEDY and len(scores_debug) >= 2:
                # pick from top-3 weighted by score (or fewer if not available)
                top_k = scores_debug[: min(3, len(scores_debug))]
                weights = [max(0.0, s - top_k[-1][0] + 1e-6) for (s, a) in top_k]
                total_w = sum(weights)
                if total_w > 0:
                    r = random.random() * total_w
                    cum = 0.0
                    for w, (_s, a) in zip(weights, top_k):
                        cum += w
                        if r <= cum:
                            chosen = a
                            break
                    else:
                        chosen = top_k[0][1]
                else:
                    chosen = scores_debug[0][1]
                if self.DEBUG:
                    self.debug_print(f"FooPlayer: EPSILON pick triggered, chosen alternate action {chosen}")
                return chosen

            # If tie, break ties preferring settlement/road/resource diversity improvements
            if len(best_actions) > 1:
                tie_metrics = []
                for a in best_actions:
                    try:
                        metric = 0.0
                        metric += self.settlement_potential(a, game, color)
                        metric += self.road_connection_potential(a, game, color)
                        # small production proxy via heuristic
                        metric += 0.01 * self._heuristic_value(game, color)
                        tie_metrics.append((metric, a))
                    except Exception:
                        tie_metrics.append((0.0, a))
                tie_metrics.sort(key=lambda x: x[0], reverse=True)
                # pick the top metric actions (could still be multiple)
                top_metric = tie_metrics[0][0]
                filtered = [a for (m, a) in tie_metrics if abs(m - top_metric) <= self.TOLERANCE]
                if filtered:
                    chosen = random.choice(filtered)
                else:
                    chosen = random.choice(best_actions)
            else:
                chosen = best_actions[0]

            # Debug logging: phase and top candidates
            if self.DEBUG:
                phase = "EARLY" if early else "MID/LATE"
                self.debug_print(f"FooPlayer: Phase={phase}, SettlementsTarget={self.TARGET_SETTLEMENTS_EARLY}")
                topn = scores_debug[:3]
                self.debug_print("FooPlayer: Top candidates:")
                for sc, act in topn:
                    self.debug_print(f"  score={sc:.2f} action={act}")

            self.debug_print(f"FooPlayer: Chosen action {chosen} with expected score {best_score}")
            return chosen
        except Exception as e:
            # Protect against unexpected errors
            print("FooPlayer: Unexpected error in decide(), defaulting to first playable action. Error:", e)
            try:
                return list(playable_actions)[0]
            except Exception:
                return None
================================= Tool Message =================================
Name: write_foo

foo_player.py updated successfully
================================== Ai Message ==================================

CODER

I updated foo_player.py with the requested targeted changes:
- Stronger early-game multipliers (settlements/roads favored more).
- Forced inclusion of at least one settlement and one road in early game when present.
- Settlement potential now rewards new resource types and production (per spec).
- Road connection potential improved for expansion heuristics.
- Development-card buy prioritization (evaluate_buy_dev_card) and inclusion when sensible.
- Increased robber/knight valuation with production loss, steal EV, army progress, and forced inclusion.
- Rollout now includes a light opponent-response (simulates a greedy opponent action) to better estimate consequences.
- Added EPSILON_GREEDY randomness to reduce predictability.
- Added TARGET_SETTLEMENTS_EARLY and logic to prefer settlements until target reached.
- Preserved adapters usage, defensive attribute access, fallbacks, and simulation budget safeguards.
- DEBUG remains False by default; set FooPlayer.DEBUG = True to enable concise debugging prints.

What I changed (concise):
- Constants: EARLY_SETTLEMENT_MULT, EARLY_ROAD_MULT, EARLY_CITY_MULT, EARLY_DEV_MULT updated to stronger early bias.
- New constants: MID_* multipliers, TARGET_SETTLEMENTS_EARLY, EPSILON_GREEDY, ROLLOUT_* bonuses, TOLERANCE.
- Functions added/updated: evaluate_buy_dev_card, cheap_pre_score (rebalanced), prefilter_actions (force includes), evaluate_robber_action (more aggressive), evaluate_play_knight (army bonus), rollout_value (opponent-response), decide (epsilon-greedy, tie-breaking favoring settlement potential).

Notes and next steps:
- The code is defensive about action and state attribute names; if you encounter AttributeError tracebacks pointing to specific adapters or game object fields, paste the traceback and I'll call RESEARCHER to refine attribute access to the real API.
- If runtime appears slow due to opponent-response expansion, reduce ROLLOUT_DEPTH or SIMULATION_BUDGET accordingly.

END CODER

